"""
Binary Calendar Reader

Utility to read and decode binary calendar data files.
Provides convenient functions to query calendar data for any date.
"""

import os
from datetime import datetime, timedelta, date
from typing import Dict, List, Optional, Tuple
from calendar10k.scripts.binary_calendar import BinaryCalendarGenerator
import struct
import sys

# Add Django project to path
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))

import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'iching.settings')
django.setup()

from iching.utils import bz


class BinaryCalendarReader:
    """Reads and provides access to binary calendar data."""
    
    def __init__(self, data_dir: str = "calendar10k/data"):
        """Initialize reader with data directory."""
        self.data_dir = data_dir
        self.cache = {}  # Cache loaded year data
        self.base_date = datetime(1549, 1, 1)  # Same as generator
    
    def get_year_data(self, year: int, use_cache: bool = True) -> Tuple[str, List[Dict]]:
        """Load year data from binary file, with optional caching."""
        if use_cache and year in self.cache:
            return self.cache[year]
        
        generator = BinaryCalendarGenerator(self.data_dir)
        version, data = generator.read_year_file(year)
        
        if use_cache:
            self.cache[year] = (version, data)
        
        return version, data
    
    def get_date_info(self, target_date: date) -> Optional[Dict]:
        """Get calendar information for a specific date."""
        year = target_date.year
        
        if not (1550 <= year <= 2648):
            return None
        
        try:
            version, year_data = self.get_year_data(year)
            
            # Find the specific date
            for day_info in year_data:
                if day_info['normal_date'].date() == target_date:
                    return {
                        'date': target_date,
                        'version': version,
                        'normal_days': day_info['normal_days'],
                        'lunar_date': day_info['lunar_date'].date(),
                        'lunar_days': day_info['lunar_days'],
                        'leap_flag': day_info['leap_flag'],
                        'jc_index': day_info['jc_index'],
                        'jq_index': day_info['jq_index'],
                        'jq_time': day_info['jq_time_formatted'],
                        'is_solar_term': day_info['jq_time_formatted'] != "00:00",
                    }
            
            return None
            
        except FileNotFoundError:
            return None
    
    def get_date_range(self, start_date: date, end_date: date) -> List[Dict]:
        """Get calendar data for a date range."""
        results = []
        start_year = start_date.year
        end_year = end_date.year
        
        # Load data for all required years
        for year in range(start_year, end_year + 1):
            try:
                version, solar_terms, days_data = self.get_optimized_year_data(year)
                
                # Filter days within the requested range
                for day_info in days_data:
                    day_date = day_info['normal_date'].date()
                    if start_date <= day_date <= end_date:
                        # Check if solar term starts on this date
                        active_term = day_info['active_solar_term']
                        is_term_start = False
                        if active_term:
                            # Check if term starts on this exact date
                            is_term_start = active_term['date'] == day_date
                        
                        # Convert to expected format for compatibility
                        results.append({
                            'date': day_date,
                            'lunar_date': day_info['lunar_date'].date(),
                            'leap_flag': day_info['leap_flag'],
                            'jc_index': day_info['jc_index'],
                            'jq_index': active_term['index'] if active_term else 0,
                            'jq_time': active_term['time'] if active_term and is_term_start else '00:00:00',
                            'is_solar_term': is_term_start,
                            'version': version
                        })
            except FileNotFoundError:
                # Skip years without data
                continue
        
        return results
    
    def get_solar_terms_in_year(self, year: int) -> List[Dict]:
        """Get all solar terms in a specific year."""
        try:
            version, year_data = self.get_year_data(year)
            solar_terms = []
            
            for day_info in year_data:
                if day_info['jq_time_formatted'] != "00:00":
                    solar_terms.append({
                        'date': day_info['normal_date'].date(),
                        'jq_index': day_info['jq_index'],
                        'jq_time': day_info['jq_time_formatted'],
                        'name': self.get_solar_term_name(day_info['jq_index'])
                    })
            
            return sorted(solar_terms, key=lambda x: x['jq_index'])
            
        except FileNotFoundError:
            return []
    
    def get_solar_term_name(self, index: int) -> str:
        """Get the name of a solar term by its index (0-23) - calendar year order."""
        solar_term_names = [
            "小寒", "大寒",                                          # January: 0-1
            "立春", "雨水", "惊蛰", "春分", "清明", "谷雨",           # Spring: 2-7
            "立夏", "小满", "芒种", "夏至", "小暑", "大暑",           # Summer: 8-13
            "立秋", "处暑", "白露", "秋分", "寒露", "霜降",           # Autumn: 14-19
            "立冬", "小雪", "大雪", "冬至"                           # Winter: 20-23
        ]
        
        if 0 <= index <= 23:
            return solar_term_names[index]
        return f"Unknown({index})"
    
    def get_jian_chu_name(self, index: int) -> str:
        """Get the name of a Jian Chu by its index (0-11)."""
        jian_chu_names = [
            "建", "除", "满", "平", "定", "执",      # 0-5
            "破", "危", "成", "收", "开", "闭"       # 6-11
        ]
        
        if 0 <= index <= 11:
            return jian_chu_names[index]
        return f"Unknown({index})"
    
    def days_since_base(self, target_date: date) -> int:
        """Calculate days since base date (same as generator)."""
        if isinstance(target_date, date) and not isinstance(target_date, datetime):
            target_date = datetime.combine(target_date, datetime.min.time())
        return (target_date - self.base_date).days
    
    def date_from_days(self, days: int) -> date:
        """Convert days since base to date."""
        return (self.base_date + timedelta(days=days)).date()
    
    def get_file_info(self, year: int) -> Optional[Dict]:
        """Get information about a binary file."""
        filename = os.path.join(self.data_dir, f"{year}.bin")
        
        if not os.path.exists(filename):
            return None
        
        try:
            version, data = self.get_year_data(year)
            file_size = os.path.getsize(filename)
            
            return {
                'year': year,
                'filename': filename,
                'version': version,
                'file_size': file_size,
                'days_count': len(data),
                'size_per_day': file_size // len(data) if data else 0,
                'date_range': f"{data[0]['normal_date'].date()} to {data[-1]['normal_date'].date()}" if data else "No data"
            }
            
        except Exception as e:
            return {'year': year, 'filename': filename, 'error': str(e)}
    
    def list_available_years(self) -> List[int]:
        """List all available years in the data directory."""
        years = []
        
        if not os.path.exists(self.data_dir):
            return years
        
        for filename in os.listdir(self.data_dir):
            if filename.endswith('.bin'):
                try:
                    year = int(filename[:-4])  # Remove .bin extension
                    if 1550 <= year <= 2648:
                        years.append(year)
                except ValueError:
                    continue
        
        return sorted(years)


class OptimizedBinaryCalendarReader:
    """
    Optimized binary calendar reader for v2.0+ format.
    Handles the space-optimized format with solar terms table and compressed day data.
    """
    
    def __init__(self, data_dir: str = "calendar10k/data"):
        self.data_dir = data_dir
        self.base_date = datetime(1549, 1, 1)
        self.cache = {}

    def get_optimized_year_data(self, year: int, use_cache: bool = True) -> Tuple[str, Dict, List[Dict]]:
        """Read and parse optimized binary file for a specific year."""
        if use_cache and year in self.cache:
            return self.cache[year]
        
        filename = os.path.join(self.data_dir, f"{year}.bin")
        
        if not os.path.exists(filename):
            raise FileNotFoundError(f"Optimized binary file for year {year} not found: {filename}")
        
        with open(filename, 'rb') as f:
            # Read header (8 bytes)
            header = f.read(8)
            version = self.parse_file_header(header)
            
            # Read solar terms table (60 bytes)
            solar_terms_data = f.read(60)
            solar_terms = self.unpack_solar_terms_table(solar_terms_data, year)
            
            # Read day data (4 bytes per day)
            days_data = []
            while True:
                data = f.read(4)
                if len(data) != 4:
                    break
                
                day_info = self.unpack_optimized_day_data(data)
                
                # Convert back to dates for verification
                normal_date = self.base_date + timedelta(days=day_info['normal_days'])
                lunar_days = day_info['normal_days'] - day_info['lunar_day_diff']
                lunar_date = self.base_date + timedelta(days=lunar_days)
                
                # Determine active solar term for this day
                active_solar_term = self.find_active_solar_term(normal_date.date(), solar_terms)
                
                day_info.update({
                    'normal_date': normal_date,
                    'lunar_date': lunar_date,
                    'active_solar_term': active_solar_term
                })
                
                days_data.append(day_info)
        
        result = (version, solar_terms, days_data)
        if use_cache:
            self.cache[year] = result
        
        return result

    def parse_file_header(self, header: bytes) -> str:
        """Parse 8-byte header to extract version."""
        major, minor, patch = struct.unpack('<BBB', header[:3])
        return f"{major}.{minor}.{patch}"

    def unpack_solar_terms_table(self, data: bytes, year: int) -> Dict:
        """Unpack 60-byte solar terms table with MM/DD/time format."""
        solar_terms = {}
        
        # Read 12 × 5-byte chunks (24 terms, 2 per chunk)
        for i in range(12):
            offset = i * 5
            
            # Read 40 bits (5 bytes) containing 2 terms
            chunk_data = data[offset:offset+5]
            combined_40bit = 0
            for byte_idx in range(5):
                combined_40bit |= chunk_data[byte_idx] << (byte_idx * 8)
            
            # Extract two 20-bit terms
            term1_packed = combined_40bit & 0xFFFFF        # First 20 bits
            term2_packed = (combined_40bit >> 20) & 0xFFFFF # Next 20 bits
            
            for j, term_packed in enumerate([term1_packed, term2_packed]):
                term_index = i * 2 + j
                if term_index >= 24:
                    break
                
                # Extract fields: 4-bit month + 5-bit day + 11-bit minutes
                minutes = term_packed & 0x7FF               # 11 bits
                day = (term_packed >> 11) & 0x1F             # 5 bits
                month = (term_packed >> 16) & 0xF            # 4 bits
                
                if month >= 1 and month <= 12 and day >= 1 and day <= 31 and minutes > 0:
                    try:
                        term_date = datetime(year, month, day).date()
                        term_time = f"{minutes//60:02d}:{minutes%60:02d}:00"
                        
                        solar_terms[term_index] = {
                            'index': term_index,
                            'date': term_date,
                            'time': term_time,
                            'name': self.get_solar_term_name(term_index)
                        }
                    except ValueError:
                        # Invalid date (e.g., Feb 30), skip
                        pass
        
        return solar_terms

    def unpack_optimized_day_data(self, data: bytes) -> Dict[str, int]:
        """Unpack 4 bytes back to optimized day data."""
        packed = struct.unpack('<I', data)[0]
        
        return {
            'normal_days': packed & 0x7FFFF,                    # 19 bits
            'lunar_day_diff': (packed >> 19) & 0x7F,            # 7 bits  
            'leap_flag': (packed >> 26) & 0x1,                  # 1 bit
            'jc_index': (packed >> 27) & 0xF,                   # 4 bits
        }

    def find_active_solar_term(self, target_date: date, solar_terms: Dict) -> Dict:
        """Find the solar term that is active on the given date."""
        # Sort solar terms by date
        sorted_terms = sorted(solar_terms.values(), key=lambda x: x['date'])
        
        active_term = None
        for term in sorted_terms:
            if term['date'] <= target_date:
                active_term = term
            else:
                break
        
        return active_term or sorted_terms[-1] if sorted_terms else None

    def get_optimized_date_info(self, target_date: date) -> Optional[Dict]:
        """Get calendar information for a specific date from optimized format."""
        year = target_date.year
        
        if not (1550 <= year <= 2648):
            return None
        
        try:
            version, solar_terms, year_data = self.get_optimized_year_data(year)
            
            # Find the specific date
            for day_info in year_data:
                if day_info['normal_date'].date() == target_date:
                    active_term = day_info['active_solar_term']
                    
                    return {
                        'date': target_date,
                        'version': version,
                        'normal_days': day_info['normal_days'],
                        'lunar_date': day_info['lunar_date'].date(),
                        'lunar_day_diff': day_info['lunar_day_diff'],
                        'leap_flag': day_info['leap_flag'],
                        'jc_index': day_info['jc_index'],
                        'jq_index': active_term['index'] if active_term else 0,
                        'jq_time': active_term['time'] if active_term and active_term['date'] == target_date else "00:00:00",
                        'is_solar_term': active_term['date'] == target_date if active_term else False,
                    }
            
            return None
            
        except FileNotFoundError:
            return None

    def get_optimized_solar_terms_in_year(self, year: int) -> List[Dict]:
        """Get all solar terms in a specific year from optimized format."""
        try:
            version, solar_terms, year_data = self.get_optimized_year_data(year)
            
            # Convert to list format
            terms_list = []
            for term_info in solar_terms.values():
                terms_list.append({
                    'date': term_info['date'],
                    'jq_index': term_info['index'],
                    'jq_time': term_info['time'],
                    'name': term_info['name']
                })
            
            return sorted(terms_list, key=lambda x: x['jq_index'])
            
        except FileNotFoundError:
            return []

    def get_solar_term_name(self, index: int) -> str:
        """Get the name of a solar term by its index (0-23) - calendar year order."""
        solar_term_names = [
            "小寒", "大寒",                                          # January: 0-1
            "立春", "雨水", "惊蛰", "春分", "清明", "谷雨",           # Spring: 2-7
            "立夏", "小满", "芒种", "夏至", "小暑", "大暑",           # Summer: 8-13
            "立秋", "处暑", "白露", "秋分", "寒露", "霜降",           # Autumn: 14-19
            "立冬", "小雪", "大雪", "冬至"                           # Winter: 20-23
        ]
        
        if 0 <= index <= 23:
            return solar_term_names[index]
        return f"Unknown({index})"

    def get_jian_chu_name(self, index: int) -> str:
        """Get the name of a Jian Chu by its index (0-11)."""
        jian_chu_names = [
            "建", "除", "满", "平", "定", "执",      # 0-5
            "破", "危", "成", "收", "开", "闭"       # 6-11
        ]
        
        if 0 <= index <= 11:
            return jian_chu_names[index]
        return f"Unknown({index})"

    def get_optimized_file_info(self, year: int) -> Optional[Dict]:
        """Get information about an optimized binary file."""
        filename = os.path.join(self.data_dir, f"{year}.bin")
        
        if not os.path.exists(filename):
            return None
        
        try:
            version, solar_terms, data = self.get_optimized_year_data(year)
            file_size = os.path.getsize(filename)
            
            return {
                'year': year,
                'filename': filename,
                'version': version,
                'file_size': file_size,
                'days_count': len(data),
                'solar_terms_count': len(solar_terms),
                'header_size': 8,
                'solar_terms_table_size': 60,
                'day_data_size': len(data) * 4,
                'size_per_day': 4,
                'date_range': f"{data[0]['normal_date'].date()} to {data[-1]['normal_date'].date()}" if data else "No data"
            }
            
        except Exception as e:
            return {'year': year, 'filename': filename, 'error': str(e)}

    def get_date_info(self, target_date: date) -> Optional[Dict]:
        """Get calendar information for a specific date (wrapper for compatibility)."""
        return self.get_optimized_date_info(target_date)
    
    def get_solar_terms_in_year(self, year: int) -> List[Dict]:
        """Get all solar terms in a year (wrapper for compatibility)."""
        return self.get_optimized_solar_terms_in_year(year)
    
    def get_file_info(self, year: int) -> Optional[Dict]:
        """Get file information (wrapper for compatibility)."""
        return self.get_optimized_file_info(year)
    
    def get_year_data(self, year: int) -> Tuple[str, List[Dict]]:
        """Get year data (wrapper for compatibility)."""
        version, solar_terms, days_data = self.get_optimized_year_data(year)
        return version, days_data
    
    def get_date_range(self, start_date: date, end_date: date) -> List[Dict]:
        """Get calendar data for a date range."""
        results = []
        start_year = start_date.year
        end_year = end_date.year
        
        # Load data for all required years
        for year in range(start_year, end_year + 1):
            try:
                version, solar_terms, days_data = self.get_optimized_year_data(year)
                
                # Filter days within the requested range
                for day_info in days_data:
                    day_date = day_info['normal_date'].date()
                    if start_date <= day_date <= end_date:
                        # Check if solar term starts on this date
                        active_term = day_info['active_solar_term']
                        is_term_start = False
                        if active_term:
                            # Check if term starts on this exact date
                            is_term_start = active_term['date'] == day_date
                        
                        # Convert to expected format for compatibility
                        results.append({
                            'date': day_date,
                            'lunar_date': day_info['lunar_date'].date(),
                            'leap_flag': day_info['leap_flag'],
                            'jc_index': day_info['jc_index'],
                            'jq_index': active_term['index'] if active_term else 0,
                            'jq_time': active_term['time'] if active_term and is_term_start else '00:00:00',
                            'is_solar_term': is_term_start,
                            'version': version
                        })
            except FileNotFoundError:
                # Skip years without data
                continue
        
        return results

    def unpack_solar_terms_table_legacy(self, data: bytes, year: int) -> Dict[int, Dict]:
        """Unpack legacy solar terms from 60 bytes (24 terms × 20 bits each) - versions before v4.0.0."""
        if len(data) != 60:
            raise ValueError("Legacy solar terms table must be exactly 60 bytes")
        
        solar_terms = {}
        
        for index in range(24):
            # Every 2 terms = 40 bits = 5 bytes (legacy format)
            byte_offset = (index // 2) * 5
            bit_offset = (index % 2) * 20
            
            if bit_offset == 0:
                # First term in the 5-byte group (bits 0-19)
                term_bits = (data[byte_offset] << 12) | (data[byte_offset + 1] << 4) | ((data[byte_offset + 2] >> 4) & 0xF)
            else:
                # Second term in the 5-byte group (bits 20-39)
                term_bits = ((data[byte_offset + 2] & 0xF) << 16) | (data[byte_offset + 3] << 8) | data[byte_offset + 4]
            
            if term_bits == 0:
                continue  # No data for this term
            
            # Unpack 20 bits: month(4) + day(5) + minutes(11)
            month = (term_bits >> 16) & 0xF
            day = (term_bits >> 11) & 0x1F
            minutes = term_bits & 0x7FF
            
            # Validate ranges
            if not (1 <= month <= 12 and 1 <= day <= 31 and 0 <= minutes <= 1439):
                continue  # Invalid data, skip
            
            # Convert minutes to time string (legacy format with no seconds)
            hours = minutes // 60
            mins = minutes % 60
            time_str = f"{hours:02d}:{mins:02d}:00"
            
            solar_terms[index] = {
                'index': index,
                'month': month,
                'day': day,
                'time': time_str,
                'name': self.solar_term_names[index]
            }
        
        return solar_terms


class PositionalBinaryCalendarReader:
    """
    Reader for positional binary calendar files (version 2.0.0+).
    
    File format:
    - Header: 8 bytes (version + year)
    - Solar Terms: 78 bytes (24 terms × 26 bits each)
    - Day Data: 16 bits per day (2 bytes per day, direct lunar MM/DD storage)
    """
    
    BASE_YEAR = 1549
    BASE_DATE = datetime(1549, 1, 1)
    
    def __init__(self, data_dir: str = "calendar10k/data"):
        self.data_dir = data_dir
        
        # Cached solar term names
        self.solar_term_names = [
            '小寒', '大寒', '立春', '雨水', '惊蛰', '春分',
            '清明', '谷雨', '立夏', '小满', '芒种', '夏至',
            '小暑', '大暑', '立秋', '处暑', '白露', '秋分',
            '寒露', '霜降', '立冬', '小雪', '大雪', '冬至'
        ]
        
        # Cached jian chu names
        self.jian_chu_names = [
            '建', '除', '满', '平', '定', '执',
            '破', '危', '成', '收', '开', '闭'
        ]
    
    def days_since_base(self, date_obj: datetime) -> int:
        """Calculate days since base date."""
        return (date_obj - self.BASE_DATE).days
    
    def parse_file_header(self, header: bytes) -> Tuple[str, int]:
        """Parse positional file header to extract version and year."""
        if len(header) < 8:
            raise ValueError("Invalid header size")
        
        # Unpack: version(3 bytes) + year(2 bytes) + padding(3 bytes)
        major, minor, patch, year, _ = struct.unpack('<BBBHB2x', header)
        version = f"{major}.{minor}.{patch}"
        
        return version, year
    
    def unpack_solar_terms_table(self, data: bytes) -> Dict[int, Dict]:
        """Unpack solar terms from 78 bytes (24 terms × 26 bits each)."""
        expected_bytes = 78  # 24 × 26 bits = 624 bits = 78 bytes
        if len(data) != expected_bytes:
            raise ValueError(f"Solar terms table must be exactly {expected_bytes} bytes, got {len(data)}")
        
        solar_terms = {}
        
        for index in range(24):
            # Calculate bit position for this term (26 bits per term)
            bit_offset = index * 26
            
            # Extract 26 bits from the byte array
            term_bits = 0
            remaining_bits = 26
            current_bit_offset = bit_offset
            
            while remaining_bits > 0:
                byte_offset = current_bit_offset // 8
                bit_in_byte = current_bit_offset % 8
                
                if byte_offset >= len(data):
                    break
                
                # How many bits to extract from current byte?
                bits_in_current_byte = min(8 - bit_in_byte, remaining_bits)
                
                # Extract bits from current byte
                byte_value = data[byte_offset]
                bits_mask = ((1 << bits_in_current_byte) - 1) << bit_in_byte
                extracted_bits = (byte_value & bits_mask) >> bit_in_byte
                
                # Add to term_bits
                term_bits |= extracted_bits << (26 - remaining_bits)
                
                # Move to next position
                remaining_bits -= bits_in_current_byte
                current_bit_offset += bits_in_current_byte
            
            if term_bits == 0:
                continue  # No data for this term
            
            # Unpack 26 bits: month(4) + day(5) + hour(5) + minute(6) + second(6)
            month = (term_bits >> 22) & 0xF     # 4 bits
            day = (term_bits >> 17) & 0x1F      # 5 bits
            hour = (term_bits >> 12) & 0x1F     # 5 bits
            minute = (term_bits >> 6) & 0x3F    # 6 bits
            second = term_bits & 0x3F           # 6 bits
            
            # Validate ranges
            if not (1 <= month <= 12 and 1 <= day <= 31 and 0 <= hour <= 23 and 0 <= minute <= 59 and 0 <= second <= 59):
                continue  # Invalid data, skip
            
            # Convert to time string with full precision
            time_str = f"{hour:02d}:{minute:02d}:{second:02d}"
            
            solar_terms[index] = {
                'index': index,
                'month': month,
                'day': day,
                'time': time_str,
                'name': self.solar_term_names[index]
            }
        
        return solar_terms
    
    def unpack_solar_terms_table_legacy(self, data: bytes) -> Dict[int, Dict]:
        """Unpack solar terms from 60 bytes (24 terms × 20 bits each for v3.0.0 format)."""
        expected_bytes = 60  # 24 × 20 bits = 480 bits = 60 bytes
        if len(data) != expected_bytes:
            raise ValueError(f"Legacy solar terms table must be exactly {expected_bytes} bytes, got {len(data)}")
        
        solar_terms = {}
        
        for index in range(24):
            # Calculate bit position for this term (20 bits per term)
            bit_offset = index * 20
            
            # Extract 20 bits from the byte array
            term_bits = 0
            remaining_bits = 20
            current_bit_offset = bit_offset
            
            while remaining_bits > 0:
                byte_offset = current_bit_offset // 8
                bit_in_byte = current_bit_offset % 8
                
                if byte_offset >= len(data):
                    break
                
                # How many bits to extract from current byte?
                bits_in_current_byte = min(8 - bit_in_byte, remaining_bits)
                
                # Extract bits from current byte
                byte_value = data[byte_offset]
                bits_mask = ((1 << bits_in_current_byte) - 1) << bit_in_byte
                extracted_bits = (byte_value & bits_mask) >> bit_in_byte
                
                # Add to term_bits
                term_bits |= extracted_bits << (20 - remaining_bits)
                
                # Move to next position
                remaining_bits -= bits_in_current_byte
                current_bit_offset += bits_in_current_byte
            
            if term_bits == 0:
                continue  # No data for this term
            
            # Unpack 20 bits: month(4) + day(5) + hour(5) + minute(6)
            month = (term_bits >> 16) & 0xF     # 4 bits
            day = (term_bits >> 11) & 0x1F      # 5 bits
            hour = (term_bits >> 6) & 0x1F      # 5 bits
            minute = term_bits & 0x3F           # 6 bits
            
            # Validate ranges
            if not (1 <= month <= 12 and 1 <= day <= 31 and 0 <= hour <= 23 and 0 <= minute <= 59):
                continue  # Invalid data, skip
            
            # Convert to time string (legacy format has no seconds)
            time_str = f"{hour:02d}:{minute:02d}:00"
            
            solar_terms[index] = {
                'index': index,
                'month': month,
                'day': day,
                'time': time_str,
                'name': self.solar_term_names[index]
            }
        
        return solar_terms
    
    def unpack_day_data_positional(self, data: bytes, year: int) -> List[Dict]:
        """Unpack positional day data from bytes."""
        days_in_year = 366 if self.is_leap_year(year) else 365
        
        # Check if this is the new 18-bit format (v3.0.0+) or old 16-bit format (v2.0.0)
        # 18-bit format: 18 bits per day, bit-packed
        # 16-bit format: 16 bits per day, byte-aligned (2 bytes per day)
        
        # Calculate expected sizes for both formats
        expected_bytes_18bit = (days_in_year * 18 + 7) // 8  # Bit-packed 18-bit format
        expected_bytes_16bit = days_in_year * 2  # Byte-aligned 16-bit format
        
        # Determine format based on data size
        is_18bit_format = abs(len(data) - expected_bytes_18bit) < abs(len(data) - expected_bytes_16bit)
        
        if is_18bit_format:
            return self._unpack_18bit_day_data(data, year)
        else:
            return self._unpack_16bit_day_data(data, year)
    
    def _unpack_18bit_day_data(self, data: bytes, year: int) -> List[Dict]:
        """Unpack 18-bit day data (v3.0.0+ with YBP support)."""
        days_in_year = 366 if self.is_leap_year(year) else 365
        unpacked_days = []
        
        for day_index in range(days_in_year):
            # Calculate bit position for this day (18 bits per day)
            bit_offset = day_index * 18
            byte_offset = bit_offset // 8
            bit_shift = bit_offset % 8
            
            # Read 3 bytes to ensure we have all 18 bits
            if byte_offset + 2 >= len(data):
                break  # Not enough data
            
            # Extract 18 bits across byte boundaries
            day_bits = 0
            for i in range(3):
                if byte_offset + i < len(data):
                    day_bits |= data[byte_offset + i] << (i * 8)
            
            # Shift to align the 18-bit value
            day_bits = (day_bits >> bit_shift) & 0x3FFFF  # 18 bits mask
            
            # Unpack 18 bits: lunar_month(4) + lunar_day(5) + leap(1) + jianchu(4) + ybp(4)
            lunar_month = (day_bits >> 14) & 0xF   # 4 bits at position 14-17
            lunar_day = (day_bits >> 9) & 0x1F     # 5 bits at position 9-13
            leap_flag = (day_bits >> 8) & 0x1      # 1 bit at position 8
            jc_index = (day_bits >> 4) & 0xF       # 4 bits at position 4-7
            ybp_index = day_bits & 0xF             # 4 bits at position 0-3
            
            # Calculate actual Gregorian date
            year_start = datetime(year, 1, 1)
            actual_date = year_start + timedelta(days=day_index)
            
            # Determine correct lunar year
            if actual_date.month < 4 and lunar_month >= 10:
                lunar_year = actual_date.year - 1
            else:
                lunar_year = actual_date.year
            
            lunar_date_str = f"{lunar_year:04d}-{lunar_month:02d}-{lunar_day:02d}"
            
            unpacked_days.append({
                'date': actual_date.date(),
                'lunar_date': lunar_date_str,
                'lunar_date_str': lunar_date_str,
                'lunar_year': lunar_year,
                'lunar_month': lunar_month,
                'lunar_day': lunar_day,
                'leap_flag': bool(leap_flag),
                'jc_index': jc_index,
                'ybp_index': ybp_index,
                'day_index': day_index
            })
        
        return unpacked_days
    
    def _unpack_16bit_day_data(self, data: bytes, year: int) -> List[Dict]:
        """Unpack 16-bit day data (v2.0.0 format, no YBP)."""
        days_in_year = 366 if self.is_leap_year(year) else 365
        expected_bytes = days_in_year * 2  # 16 bits per day = 2 bytes per day
        
        if len(data) < expected_bytes:
            raise ValueError(f"Day data too short: {len(data)} bytes, expected at least {expected_bytes}")
        
        unpacked_days = []
        
        for day_index in range(days_in_year):
            # Calculate byte position for this day (2 bytes per day)
            byte_offset = day_index * 2
            
            if byte_offset + 1 >= len(data):
                break  # Not enough data
            
            # Read 2 bytes (16 bits, little-endian)
            day_bits = data[byte_offset] | (data[byte_offset + 1] << 8)
            
            # Unpack 16 bits: lunar_month(4) + lunar_day(5) + leap(1) + jianchu(4) + unused(2)
            lunar_month = (day_bits >> 10) & 0xF  # 4 bits at position 10-13 (stored directly as 1-12)
            lunar_day = (day_bits >> 5) & 0x1F    # 5 bits at position 5-9 (stored directly as 1-31)
            leap_flag = (day_bits >> 4) & 0x1     # 1 bit at position 4
            jc_index = day_bits & 0xF             # 4 bits at position 0-3
            
            # Calculate actual Gregorian date
            year_start = datetime(year, 1, 1)
            actual_date = year_start + timedelta(days=day_index)
            
            # Determine correct lunar year based on Gregorian date and lunar month
            if actual_date.month < 4 and lunar_month >= 10:
                lunar_year = actual_date.year - 1
            else:
                lunar_year = actual_date.year
            
            lunar_date_str = f"{lunar_year:04d}-{lunar_month:02d}-{lunar_day:02d}"
            
            unpacked_days.append({
                'date': actual_date.date(),
                'lunar_date': lunar_date_str,
                'lunar_date_str': lunar_date_str,
                'lunar_year': lunar_year,
                'lunar_month': lunar_month,
                'lunar_day': lunar_day,
                'leap_flag': bool(leap_flag),
                'jc_index': jc_index,
                'day_index': day_index
            })
        
        return unpacked_days
    
    def is_leap_year(self, year: int) -> bool:
        """Check if year is a leap year."""
        return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
    
    def get_positional_year_data(self, year: int) -> Tuple[str, Dict, List[Dict]]:
        """Read and parse positional binary calendar file for a specific year."""
        filename = os.path.join(self.data_dir, f"{year}.bin")
        
        if not os.path.exists(filename):
            raise FileNotFoundError(f"Calendar file not found: {filename}")
        
        with open(filename, 'rb') as f:
            # Read header (8 bytes)
            header = f.read(8)
            if len(header) != 8:
                raise ValueError("Invalid file format: incomplete header")
            
            version, file_year = self.parse_file_header(header)
            
            if file_year != year:
                raise ValueError(f"File year mismatch: expected {year}, got {file_year}")
            
            # Determine solar terms table size based on version
            major_version = int(version.split('.')[0])
            if major_version >= 4:
                # v4.0.0+ uses 78 bytes for HH:MM:SS precision
                solar_terms_size = 78
            else:
                # Older versions use 60 bytes for minute precision
                solar_terms_size = 60
            
            # Read solar terms table
            solar_terms_data = f.read(solar_terms_size)
            if len(solar_terms_data) != solar_terms_size:
                raise ValueError(f"Invalid file format: incomplete solar terms table (expected {solar_terms_size} bytes)")
            
            if major_version >= 4:
                solar_terms = self.unpack_solar_terms_table(solar_terms_data)
            else:
                solar_terms = self.unpack_solar_terms_table_legacy(solar_terms_data)
            
            # Read remaining data (day data)
            day_data_bytes = f.read()
            year_data = self.unpack_day_data_positional(day_data_bytes, year)
        
        return version, solar_terms, year_data
    
    def get_date_info_positional(self, target_date: date) -> Optional[Dict]:
        """Get calendar information for a specific date using positional format."""
        year = target_date.year
        
        try:
            version, solar_terms, year_data = self.get_positional_year_data(year)
        except (FileNotFoundError, ValueError):
            return None
        
        # Calculate day index within the year
        year_start = date(year, 1, 1)
        day_index = (target_date - year_start).days
        
        if day_index < 0 or day_index >= len(year_data):
            return None
        
        day_info = year_data[day_index]
        
        # Find current solar term
        jq_index = 0
        jq_time = "00:00:00"
        is_solar_term = False
        
        # Check if this date has a solar term
        for term_index, term_data in solar_terms.items():
            term_date = date(year, term_data['month'], term_data['day'])
            if term_date == target_date:
                jq_index = term_index
                jq_time = term_data['time']
                is_solar_term = True
                break
        
        # If not a solar term start, find the most recent solar term
        if not is_solar_term:
            recent_term = None
            for term_index, term_data in solar_terms.items():
                term_date = date(year, term_data['month'], term_data['day'])
                if term_date <= target_date:
                    if recent_term is None or term_date > recent_term[1]:
                        recent_term = (term_index, term_date)
            
            if recent_term:
                jq_index = recent_term[0]
        
        result = {
            'version': version,
            'date': target_date,
            'lunar_date': day_info['lunar_date_str'],
            'lunar_year': day_info['lunar_year'],
            'lunar_month': day_info['lunar_month'],
            'lunar_day': day_info['lunar_day'],
            'leap_flag': day_info['leap_flag'],
            'jc_index': day_info['jc_index'],
            'jq_index': jq_index,
            'jq_time': jq_time,
            'is_solar_term': is_solar_term,
            'day_index': day_info['day_index']
        }
        
        # Add YBP data if available (v3.0.0+ format)
        if 'ybp_index' in day_info:
            result['ybp_index'] = day_info['ybp_index']
        
        return result
    
    def get_solar_term_name(self, index: int) -> str:
        """Get solar term name by index."""
        if 0 <= index < len(self.solar_term_names):
            return self.solar_term_names[index]
        return f"Unknown({index})"
    
    def get_jian_chu_name(self, index: int) -> str:
        """Get jian chu name by index."""
        if 0 <= index < len(self.jian_chu_names):
            return self.jian_chu_names[index]
        return f"Unknown({index})"
    
    def list_available_years(self) -> List[int]:
        """List all available years in the data directory."""
        years = []
        if not os.path.exists(self.data_dir):
            return years
        
        for filename in os.listdir(self.data_dir):
            if filename.endswith('.bin') and filename[:-4].isdigit():
                year = int(filename[:-4])
                years.append(year)
        
        return sorted(years)
    
    def get_file_info(self, year: int) -> Optional[Dict]:
        """Get information about a binary calendar file."""
        filename = os.path.join(self.data_dir, f"{year}.bin")
        
        if not os.path.exists(filename):
            return {'error': 'File not found'}
        
        try:
            file_size = os.path.getsize(filename)
            
            with open(filename, 'rb') as f:
                header = f.read(8)
                version, file_year = self.parse_file_header(header)
            
            days_count = 366 if self.is_leap_year(year) else 365
            date_range = f"{year}-01-01 to {year}-12-31"
            
            return {
                'filename': os.path.basename(filename),
                'version': version,
                'file_size': file_size,
                'days_count': days_count,
                'date_range': date_range,
                'file_year': file_year
            }
        
        except Exception as e:
            return {'error': str(e)}

    def get_solar_terms_in_year_positional(self, year: int) -> List[Dict]:
        """Get all solar terms for a specific year in positional format."""
        try:
            version, solar_terms, year_data = self.get_positional_year_data(year)
        except (FileNotFoundError, ValueError):
            return []
        
        solar_terms_list = []
        for index in range(24):
            if index in solar_terms:
                term_data = solar_terms[index]
                term_date = date(year, term_data['month'], term_data['day'])
                
                solar_terms_list.append({
                    'jq_index': index,
                    'name': term_data['name'],
                    'date': term_date,
                    'jq_time': term_data['time']
                })
        
        # Sort by date
        solar_terms_list.sort(key=lambda x: x['date'])
        return solar_terms_list
    
    def get_date_range_positional(self, start_date: date, end_date: date) -> List[Dict]:
        """Get calendar information for a date range using positional format."""
        if start_date.year != end_date.year:
            raise ValueError("Date range must be within the same year for positional format")
        
        year = start_date.year
        
        try:
            version, solar_terms, year_data = self.get_positional_year_data(year)
        except (FileNotFoundError, ValueError):
            return []
        
        result = []
        year_start = date(year, 1, 1)
        
        # Calculate start and end indices
        start_index = (start_date - year_start).days
        end_index = (end_date - year_start).days
        
        if start_index < 0 or end_index >= len(year_data):
            return []
        
        for day_index in range(start_index, end_index + 1):
            day_info = year_data[day_index]
            current_date = day_info['date']
            
            # Find current solar term for this date
            jq_index = 0
            jq_time = "00:00:00"
            is_solar_term = False
            
            # Check if this date has a solar term
            for term_index, term_data in solar_terms.items():
                term_date = date(year, term_data['month'], term_data['day'])
                if term_date == current_date:
                    jq_index = term_index
                    jq_time = term_data['time']
                    is_solar_term = True
                    break
            
            # If not a solar term start, find the most recent solar term
            if not is_solar_term:
                recent_term = None
                for term_index, term_data in solar_terms.items():
                    term_date = date(year, term_data['month'], term_data['day'])
                    if term_date <= current_date:
                        if recent_term is None or term_date > recent_term[1]:
                            recent_term = (term_index, term_date)
                
                if recent_term:
                    jq_index = recent_term[0]
            
            day_result = {
                'version': version,
                'date': current_date,
                'lunar_date': day_info['lunar_date_str'],
                'lunar_year': day_info['lunar_year'],
                'lunar_month': day_info['lunar_month'],
                'lunar_day': day_info['lunar_day'],
                'leap_flag': day_info['leap_flag'],
                'jc_index': day_info['jc_index'],
                'jq_index': jq_index,
                'jq_time': jq_time,
                'is_solar_term': is_solar_term,
                'day_index': day_info['day_index']
            }
            
            # Add YBP data if available (v3.0.0+ format)
            if 'ybp_index' in day_info:
                day_result['ybp_index'] = day_info['ybp_index']
            
            result.append(day_result)
        
        return result


def demo_usage():
    """Demonstrate usage of the binary calendar reader."""
    reader = BinaryCalendarReader()
    
    print("=== Binary Calendar Reader Demo ===\n")
    
    # Check available years
    available_years = reader.list_available_years()
    print(f"Available years: {available_years[:10]}..." if len(available_years) > 10 else f"Available years: {available_years}")
    
    if not available_years:
        print("No binary calendar files found. Please generate some first.")
        return
    
    # Demo 1: Get info for a specific date
    test_date = date(2025, 3, 5)  # Spring Equinox period
    print(f"\n1. Calendar info for {test_date}:")
    
    date_info = reader.get_date_info(test_date)
    if date_info:
        print(f"   Normal date: {date_info['date']}")
        print(f"   Lunar date: {date_info['lunar_date']}")
        print(f"   Solar term: {reader.get_solar_term_name(date_info['jq_index'])} (#{date_info['jq_index']})")
        print(f"   Jian Chu: {reader.get_jian_chu_name(date_info['jc_index'])} (#{date_info['jc_index']})")
        print(f"   Leap month: {'Yes' if date_info['leap_flag'] else 'No'}")
        if date_info['is_solar_term']:
            print(f"   *** Solar term starts at {date_info['jq_time']} ***")
    else:
        print(f"   No data found for {test_date}")
    
    # Demo 2: Get solar terms for a year
    test_year = available_years[0]
    print(f"\n2. Solar terms in {test_year}:")
    
    solar_terms = reader.get_solar_terms_in_year(test_year)
    for term in solar_terms[:6]:  # Show first 6
        print(f"   {term['name']} (#{term['jq_index']}): {term['date']} at {term['jq_time']}")
    
    if len(solar_terms) > 6:
        print(f"   ... and {len(solar_terms) - 6} more")
    
    # Demo 3: File information
    print(f"\n3. File information for {test_year}:")
    file_info = reader.get_file_info(test_year)
    if file_info and 'error' not in file_info:
        print(f"   File: {file_info['filename']}")
        print(f"   Version: {file_info['version']}")
        print(f"   Size: {file_info['file_size']:,} bytes")
        print(f"   Days: {file_info['days_count']}")
        print(f"   Bytes per day: {file_info['size_per_day']}")
        print(f"   Date range: {file_info['date_range']}")
    
    print("\n=== Demo complete ===")


if __name__ == "__main__":
    demo_usage() 