#!/usr/bin/python
# -*- coding: utf-8 -*-
'''
Stellaris Empires Parser
this is a fairly dumb parser that can parse stellaris empires from the user_empire_designs.txt, if they are set up per default
it might not reflect the total logic of paradox' internal scripting language however.

it preserves the orders, gives ability to modify values, and translates most into python
it also has the ability to dump it back into a string, which is ready to be written back.

First Draft written in Py2, PyQt4
Created on 07.04.2017

moving to py3 and qt5

@author: g4b.org
'''
import os, sys, logging

DEFAULT_WIN_EMPIRES_PATH = r'Paradox Interactive\Stellaris\user_empire_designs.txt'
OUTPUT_LINE_FEED = '\r\n'

class _ConstantCache:
    """ datamines constants """
    def __init__(self):
        self._cached = []
        self.cached = set()
        self.associations = {}
    def add(self, value):
        self._cached.append(value)
        self.cached = set(self._cached)
        self._cached = list(self.cached)
    def associate(self, key, value):
        self.associations[key] = set(list(self.associations.get(key, [])) + [value,])
_CONSTANT_CACHE = _ConstantCache()

class Constant:
    """ constants are not strings, but we need to save them somehow. """
    def __init__(self, value):
        self.value = value
        _CONSTANT_CACHE.add(value)
    @classmethod
    def show_cache(cls):
        return _CONSTANT_CACHE.cached
    @classmethod
    def associate(cls, key, value):
        _CONSTANT_CACHE.associate(key, value)
    @classmethod
    def get_associations(cls):
        return _CONSTANT_CACHE.associations

def render_value(value):
    """ renders internal python values to stellaris values, but keeps the tree as list of lists and strings """
    if isinstance(value, bool):
        return ['%s' % 'yes' if value else 'no']
    elif isinstance(value, int):
        return ['%s' % value]
    elif isinstance(value, basestring): # note: python 3 might not have basestring anymore?
        return ['"%s"' % value]
    elif isinstance(value, list):
        return ['{',] + [render_value(entry) for entry in value] + ['}' ]
    elif isinstance(value, Entry):
        return value.render()
    elif isinstance(value, Constant):
        return ['%s' % value.value]
    else:
        logging.warning('Unknown Value type: %s' % (str(type(value)),))
    
def build_final_string(data):
    """ builds a final output from render data. """
    def indent(value, ind):
        if not ind:
            return value
        ind = ind * ' '
        return '%s%s' % (ind, value)
    final = []
    def append_final_list(data, cur):
        for entry in data:
            if isinstance(entry, basestring):
                final.append(indent(entry, cur))
            elif isinstance(entry, list):
                append_final_list(entry, cur + 2)
            else:
                logging.error('Unknown Type in final list: %s' % (str(type(entry)),))
    append_final_list(data, 0)
    return OUTPUT_LINE_FEED.join(final)
    

class Entry:
    """ class for holding an entry in stellaris format """
    def __init__(self, key, value):
        self.key = key
        self.value = value
    
    @classmethod
    def identify_value(cls, value, key=None):
        """ Note: Value cannot be a list. 
            key is only used to track constants and used internally. 
        """
        if value.startswith('"'):
            return value[1:-1]
        elif value in ['yes', 'no']:
            return True if value == 'yes' else False
        else:
            try:
                x = int(value)
                return x
            except:
                if key is not None:
                    # if key is given, it only is used to associate the constant with it.
                    c = Constant(value)
                    Constant.associate(key, value)
                    return c
                return Constant(value)
        
    def parse(self, lines):
        # parses until it has its data, returns lines of buffer it ate.
        count = 0
        skip = 0
        my_value = []
        for line in lines:
            count += 1
            if skip>0:
                skip -= 1
                continue
            line = line.replace('\t', ' ' * 4)
            line = line.lstrip(' ')
            # search for key = value or (key)={
            if line.startswith('='):
                # this seems to be a ={
                continue
            elif '=' in line:
                key = line[:line.index('=')].strip()
                value = line[line.index('=')+1:].strip()
                # if its a simple entry, add it to me.
                # if its a {, we need to create a new entry with key, give it the rest of the buffer
                if value.startswith('{'):
                    entry = Entry(key, None)
                    skip = entry.parse(lines[count:])
                    my_value.append(entry)
                    continue
                else:
                    entry = Entry(key, Entry.identify_value(value, key))
                    my_value.append(entry)
                    continue
            elif line.startswith('}'):
                # if its a } we are at the end of our duty.
                break
            else:
                my_value.append(Entry.identify_value(line))
        self.value = my_value
        return count
    
    def render(self):    
        val = render_value(self.value)
        ret = [ '%s=%s' % (self.key or '', val[0]) ]
        if len(val)>1:
            ret += [val[1:-1], val[-1]]
        return ret
    
    def get_values(self, key, default=None):
        """ retrieves the list of all values in the entry by key, 
            as long as the value of this entry is a list of entries. 
        """
        if isinstance(self.value, list):
            ret = []
            for entry in self.value:
                if isinstance(entry, Entry):
                    if entry.key == key:
                        ret.append(entry.value)
            return ret or default
        return default
        

class Empire:
    """
        A Stellaris Empire configuration.
    """
    def __init__(self, name=None):
        self.name = name
        self.entry = None
    
    def get_values(self, key, default=None):
        """ use this function if you want to get a specific value from the root entry. convenience function """
        if self.entry is None:
            return default
        return self.entry.get_values(key, default)
        
    def parse(self, buff):
        """ parse my entry from a buffer """
        count = 0
        if isinstance(buff, str):
            buff = buff.replace('\r', '').split('\n')
        if self.name is None:
            for line in buff:
                count += 1
                if line.startswith('"'):
                    self.name = Entry.identify_value(line)
                    buff = buff[count:]
                    break
        self.entry = Entry(None, None)
        return count + self.entry.parse(buff)
        
    def dump(self):
        if self.entry is None:
            return ""
        if self.name is None:
            return ""
        return ('"%s"%s' % (self.name or '', OUTPUT_LINE_FEED)) + build_final_string(self.entry.render())
    
    def __unicode__(self):
        return '%s' % self.name

class UserEmpireDesigns:
    """
        main class for loading user empire designs.
    """
    def __init__(self, filename):
        self.empires = []
        self.filename = filename
        if filename is not None:
            self.parse_file()
    
    def all(self):
        return self.empires
    
    def parse_lines(self, lines):
        while len(lines) > 2: # makes no sense below 2 lines.
            empire = Empire()
            c = empire.parse(lines)
            lines = lines[c:]
            self.empires.append(empire)
        logging.debug( "Read %s empires" % len(self.empires) )
    
    def parse_file(self):
        f = open(self.filename, 'r')
        data = f.read()
        f.close()
        lines = data.replace('\r', '').split('\n')
        self.parse_lines(lines)
    
    def dump(self):
        ret = []
        for empire in self.empires:
            ret.append(empire.dump())
        return OUTPUT_LINE_FEED.join(ret)
    
def load_empires(filename=None):
    if filename is None:
        folder = find_folder_win()
        filename = os.path.join( folder, DEFAULT_WIN_EMPIRES_PATH )
        #print(f'Folder: {folder} Filename: {filename}')
    return UserEmpireDesigns(filename)

def find_folder_win(folder_id=5, current=0):
    """ Finds My Documents in windows """
    import ctypes.wintypes
    CSIDL_PERSONAL = folder_id     # My Documents = 5
    SHGFP_TYPE_CURRENT = current   # Unclear, but 1 means current, 0 default!?
    buf= ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
    ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
    return buf.value

if __name__ == '__main__':
    # if u run this script, it basicly parses the file and outputs a list of your empires, a dump, and the constants it found.
    logging.basicConfig(level=logging.DEBUG)
    empires = load_empires()
    for empire in empires.all():
        print ("%s traits: %s ethics: %s" % (empire.name, 
                           ', '.join([ x.replace('trait_', '') for x in empire.entry.get_values('trait')]),
                           ', '.join([ x.replace('ethic_', '') for x in empire.entry.get_values('ethic')]),
                           ))
    print ("#" * 80)
    print (empires.dump())
    print ("CONSTANTS:")
    print (Constant.show_cache())
    print (Constant.get_associations())
    