import argparse
from enum import Enum
from pathlib import Path



class Locations:
    def __init__(self):
        text_directory = Path("../resources/texts")
        self.texts: Path = text_directory
        self.client_changelog: Path = text_directory / "change_log_client.md"
        self.materials_changelog: Path = text_directory / "change_log_materials.md"
        self.output: Path = text_directory / "change_log.txt"


def main(loc: Locations, channel: str) -> None:
    client_cl = read_and_verify_token(loc.client_changelog)
    materials_cl = read_and_verify_token(loc.materials_changelog)
    materials_dict = get_material_dicts(materials_cl)
    merged_cl = convert(client_cl, materials_dict, write_external(channel))
    data = '\n'.join(merged_cl)
    write(loc.output, data)


def read(filepath: Path) -> str:
    with open(filepath, 'r', encoding = "utf-8") as f:
        return f.readlines()


def write(filepath: Path, changes: str) -> None:
    with open(filepath, 'w', encoding = "utf-8") as f:
        f.write(changes)


def write_external(channel: str) -> bool:
    return channel in ["external", "internal"]


class Version:
    major: int
    minor: int
    # we don't care about anything after patch for purposes of matching
    other_data: str

    def __init__(self, version_number: str):
        parts = version_number.split('.')
        m = f"Error converting version: {version_number}\n\t"
        if len(parts)<2:
            raise ValueError(f"{m}At minimum the major and minor versions are required eg: 3.1")
        
        try:
            self.major = int(parts[0])
        except:
            raise ValueError(f"{m}The first value, major version, must be a valid integer: 3.1")
        
        try:
            self.minor= int(parts[1])
        except:
            raise ValueError(f"{m}The second value, minor version, must be a valid integer: 3.1")
        
        self.other_data = '.'.join(parts[2:])
    
    @staticmethod
    def is_Version(version_number: str) -> bool:
        """Check if a version number string could become a version"""
        try:
            Version(version_number)
            return True
        except:
            return False
    
    def __eq__(self, other) -> bool:
        return self.major==other.major and self.minor==other.minor
    
    def major_minor_str(self) -> str:
        return f"{self.major}.{self.minor}"
    
    def __str__(self) -> str:
        if self.other_data=='':
            return self.major_minor_str()
        else:
            return f"{self.major_minor_str()}.{self.other_data}"
    
class LineTokens(Enum):
    EMPTY = 1
    ITEM = 2
    HEADING1 = 3
    HEADING2 = 4
    REFERENCE = 5
    
class TokenizedLine:
    token: LineTokens
    data = ''
    
    def __init__(self, line: str):
        l = line.strip()
        
        if l == '':
            self.token = LineTokens.EMPTY
        elif l.startswith('- '):
            self.token = LineTokens.ITEM
            self.data = l[2:]
        elif l.startswith('# '):
            self.token = LineTokens.HEADING1
            try:
                self.data = Version(l[2:])
            except:
                raise ValueError(f"Parsing not correct, the following heading must be a valid version number: {l}")

        elif l.startswith('## ') and l[3:].lower()=='internal':
            self.token = LineTokens.HEADING2
            self.data = "Internal"
        elif l.startswith('> '):
            self.token = LineTokens.REFERENCE
            try:
                self.data = Version(l[2:])
            except:
                raise ValueError(f"Parsing not correct, the following reference must be a valid version number: {l}")

        else:
            raise ValueError(f"Parsing not correct, the following is not a valid line: \n{l}")

def verify(lines: list[str], file_name: str='')->bool:
    """verify a changelog as correct
    1. Empty lines have no effect
        1.1 Any spacing at the start of a line must be ignored
        1.2 After a format character a single space is required
    2. Version headings must have a single `#` eg:
        # 10.5.2
    3. Second level headings `##` are always hidden and if they are not named internal, an error will occur:
        ## internal OR ## Internal
    4. Items must be in a list starting with `-`
        - item
    5. Any items under the second level heading must be hidden in the output
        - private item
    6. Only in the client changelog, a reference of a material version is allowed `> major.minor`
        > 1.1
    7. only sematic versions with major minor are allowed in references
    """
    messenger = lambda l,m: print(f"Error in file:{file_name}\n\tError in line: {l}\n\t{m}")
    
    for line in lines:
        l = line.strip()
        if l=='':
            continue

        if l[0] not in '#->' or len(l)<=3:
            messenger(l,"All lines must begin with one of the following Control characters '-' '#' '>' ")
            return False
        if l[1] not in ' #':
            messenger(l,"All control characters must have a space after them. Except after '#', where another '#' is valid")
        if l.startswith('# '):
            try:
                Version(l[2:])
            except:
                messenger(l,"Version number could not be parsed")
                return False
        if l.startswith('## ') and l[3:].lower()!='internal':
            messenger(l,"Level 2 headings can only be 'internal' (not case sensitive).")
        if l.startswith('> '):
            try:
                v = Version(l[2:])
            except:
                messenger(l,"Version number could not be parsed")
                return False
            if v.other_data!='':
                messenger(l,"References can only contain major minor version numbers eg: 3.1")
                return False
    
    return True

def read_and_verify_token(filepath: Path) -> list[TokenizedLine]:
    data = read(filepath)
    if not verify(data,filepath.name):
        raise ValueError(f"{filepath} does not meet parsing requirements")
    
    return [TokenizedLine(line) for line in data]

def get_material_dicts(materials_changelog: list[TokenizedLine]) -> dict:
    """Go through the materials changelog and split it into major minor versions"""
    materials_versions = {}
    existing_version = Version('-1.-1')
    is_second_level = False
    for t in materials_changelog:
        if t.token==LineTokens.HEADING1:
            existing_version = t.data
            s = existing_version.major_minor_str()
            if s not in materials_versions:
                materials_versions[s] = []
            is_second_level = False
            continue
        
        if t.token==LineTokens.HEADING2:
            is_second_level = True
            continue
        
        if t.token==LineTokens.ITEM and not is_second_level:
            s = existing_version.major_minor_str()
            materials_versions[s].append(t)
            continue
        
    return materials_versions

def convert_single_reference(line: TokenizedLine, materials_dict: dict) -> list[str]:
    """Convert a single reference '>' to output for the client"""
    s = line.data.major_minor_str()
    if s not in materials_dict:
        raise ValueError(f"Version reference: {line.data}, not found in materials changelog")
    
    out = [f"Materials updates {s}:"]
    for d in materials_dict[s]:
        out.append(f"- {d.data}")
    
    return out

def convert(change_log_client: list[TokenizedLine], materials_dict: dict = '', external_only: bool = False) -> list[str]:
    """Convert from the markdown tokens to the output format for the client reference replacement is done in a 
    sub-function.
    
    If no `materials_dict` is supplied then the function will fail if references are present.
    """
    out = []
    is_second_level = False
    for t in change_log_client:
        if t.token==LineTokens.HEADING1:
            if external_only and out:
                return out
            if out: # dont append empty line before first element
                out.append("")
            out.append(str(t.data))
            is_second_level = False
            continue
        
        if t.token==LineTokens.HEADING2:
            is_second_level = True
            if not external_only:
                out.append(str(t.data))
            continue
        
        if t.token==LineTokens.ITEM and not is_second_level:
            out.append(f"- {t.data}")
            continue

        if t.token==LineTokens.ITEM and is_second_level and not external_only:
            out.append(f"- {t.data}")
            continue
        
        if t.token==LineTokens.REFERENCE:
            reference_lines = convert_single_reference(t,materials_dict)
            for item in reference_lines:
                out.append(item)
    return out

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description="Pass the channel to determine what is included in the changelog output"
    )
    parser.add_argument(
        "-c", "--channel",
        help="The specific channel to freeze, e.g. stable.",
        required=True
    )
    args = parser.parse_args()

    locations = Locations()
    main(locations, args.channel)
