#!/usr/bin/env python
#===============================================================================
# Copyright (C) 2010 Anton Vorobyov
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos.  If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
 
'''
This script pulls data out of EVE cache and makes a database dump. To get most of the data,
you need to just log into game; however, for some special data sometimes you need to dump
it by executing corresponding action in game, for example - open market tree to get data for
invmarketgroups table.
Reverence library by Entity is used, check http://wiki.github.com/ntt/reverence/ for info
As reverence uses the same Python version as EVE client (2.x series), script cannot be converted to python3
Example commands to run the script under Linux with default eve paths for getting SQLite dump:
Tranquility: python eveCacheToDb.py --eve="~/.wine/drive_c/Program Files/CCP/EVE" --cache="~/.wine/drive_c/users/"$USER"/Local Settings/Application Data/CCP/EVE/c_program_files_ccp_eve_tranquility/cache" --dump="sqlite:////home/"$USER"/Desktop/eve.db"
Singularity: python eveCacheToDb.py --eve="~/.wine/drive_c/Program Files/CCP/Singularity" --cache="~/.wine/drive_c/users/"$USER"/Local Settings/Application Data/CCP/EVE/c_program_files_ccp_singularity_singularity/cache" --sisi --dump="sqlite:////home/"$USER"/Desktop/evetest.db"
'''
 
import os
import sys
 
# Add eos root path to sys.path
path = os.path.dirname(unicode(__file__, sys.getfilesystemencoding()))
sys.path.append(os.path.realpath(os.path.join(path, "..", "..", "..")))
 
def get_map():
    """
    Return table name - table class map
    """
    return {"allianceshortnames": None,
            "billtypes": None,
            "certificaterelationships": None,
            "certificates": None,
            "corptickernames": None,
            "dgmattribs": AttributeInfo,
            "dgmeffects": EffectInfo,
            "dgmtypeattribs": Attribute,
            "dgmtypeeffects": Effect,
            "evegraphics": None,
            "evelocations": None,
            "eveowners": None,
            "eveunits": Unit,
            "groupsByCategories": None,
            "icons": Icon,
            "invbptypes": None,
            "invcategories": Category,
            "invcontrabandTypesByFaction": None,
            "invcontrabandTypesByType": None,
            "invgroups": Group,
            "invmetagroups": MetaGroup,
            "invmarketgroups": MarketGroup,
            "invmetatypes": MetaType,
            "invmetatypesByTypeID": None,
            "invreactiontypes": None,
            "invtypes": Item,
            "locationscenes": None,
            "locationwormholeclasses": None,
            "mapcelestialdescriptions": None,
            "ownericons": None,
            "ramactivities": None,
            "ramaltypes": None,
            "ramaltypesdetailpercategory": None,
            "ramaltypesdetailpergroup": None,
            "ramcompletedstatuses": None,
            "ramtyperequirements": None,
            "schematics": None,
            "schematicsByPin": None,
            "schematicsByType": None,
            "schematicspinmap": None,
            "schematicstypemap": None,
            "shiptypes": None,
            "sounds": None,
            "typesByGroups": None,
            "typesByMarketGroups": None}
 
def get_order():
    """
    Return order for table processing
    """
    return ("icons",
            "invmarketgroups",
            "eveunits",
            "dgmattribs",
            "dgmeffects",
            "invcategories",
            "invgroups",
            "invmetagroups",
            "invtypes",
            "invmetatypes",
            "dgmtypeattribs",
            "dgmtypeeffects")
 
def get_customcalls():
    """
    Return custom table - call to get data for it map
    """
    return {"invmarketgroups": eve.RemoteSvc("marketProxy").GetMarketGroups()}
 
def process_table(sourcetable, tablename, tableclass):
    """
    Get all data from cache and write it to database
    """
    # Get data from source and process it
    tabledata = get_table_data(sourcetable, tablename, get_source_headers(sourcetable))
    # Insert everything into table
    insert_table_values(tabledata, tableclass)
    return
 
def get_source_headers(sourcetable):
    """
    Pull list of headers from the source table
    """
    sourceheaders = None
    guid = getattr(sourcetable, "__guid__", "None")
    # For IndexRowset and IndexedRowLists Reverence provides list of headers
    if guid in ("util.IndexRowset", "util.FilterRowset"):
        sourceheaders = tuple(sourcetable.header)
    # For IndexedRowLists, we need to compose list ourselves
    elif guid == "util.IndexedRowLists":
        headerset = set()
        for item in sourcetable:
            for row in sourcetable[item]:
                for headername in row.__header__.Keys():
                    headerset.add(headername)
        sourceheaders = tuple(headerset)
    return sourceheaders
 
def get_table_data(sourcetable, tablename, headers):
    """
    Pull data out of source table
    """
    # Each row is enclosed into dictionary, full table is list of these dictionaries
    datarows = []
    guid = getattr(sourcetable, "__guid__", "None")
    # We have Select method for IndexRowset tables
    if guid == "util.IndexRowset":
        for values in sourcetable.Select(*headers):
            # When Select is asked to find single value, it is returned in its raw
            # form. Convert is to tuple for proper further processing
            if not isinstance(values, (list, tuple, set)):
                values = (values,)
            headerslen = len(headers)
            datarow = {}
            # 1 row value should correspond to 1 header, if number or values doesn't
            # correspond to number of headers then something went wrong
            if headerslen != len(values):
                print "Error: malformed data in source table {0}".format(tablename)
                return None
            # Fill row dictionary with values and append it to list
            for i in xrange(headerslen):
                # If we've got ASCII string, convert it to Unicode
                if isinstance(values[i], str):
                    datarow[headers[i]] = unicode(values[i], 'ISO-8859-1')
                else:
                    datarow[headers[i]] = values[i]
            datarows.append(datarow)
    # FilterRowset and IndexedRowLists are accessible almost like dictionaries
    elif guid in ("util.FilterRowset", "util.IndexedRowLists"):
        # Go through all source table elements
        for element in sourcetable.iterkeys():
            # Go through all rows of an element
            for row in sourcetable[element]:
                datarow = {}
                # Fill row dictionary with values we need and append it to the list
                for header in headers:
                    value = getattr(row, header, None)
                    # None and zero values are different, and we want to write zero
                    # values to database
                    if value or value in (0, 0.0):
                        datarow[header] = value
                datarows.append(datarow)
 
    return datarows
 
def insert_table_values(tabledata, tableclass):
    """
    Insert values into tables and show progress
    """
    rows = 0
    rows_skipped = 0
    # Go through all table rows
    for row in tabledata:
        instance = tableclass()
        # Print dot each 1k inserted rows
        if rows / 1000.0 == int(rows / 1000.0):
            sys.stdout.write(".")
            sys.stdout.flush()
        try:
            # Go through all fields of a row, process them and insert
            for header in row:
                setattr(instance, header, process_value(row[header], tableclass, header))
            eos.db.gamedata_session.add(instance)
            rows += 1
        except ValueError:
            rows_skipped += 1
    # Print out results and actually commit results to database
    print "\nInserted {0} rows. skipped {1} rows".format(rows, rows_skipped)
    eos.db.gamedata_session.commit()
 
def process_value(value, tableclass, header):
    # Get column info
    info = tableclass._sa_class_manager.mapper.c.get(header)
    if info is None:
        return
 
    # Null out non-existent foreign key relations
    foreign_keys = info.foreign_keys
    if len(foreign_keys) > 0:
        for key in foreign_keys:
            col = key.column
            if not query_existence(col, value) and not key.deferrable:
                if info.nullable:
                    return None
                else:
                    raise ValueError("Integrity check failed")
            else:
                return value
    #Turn booleans into actual booleans, don't leave them as integers
    elif type(info.type) == Boolean:
        return bool(value)
    else:
        return value
 
existence_cache = {}
def query_existence(col, value):
    key = (col, col.table, value)
    info = existence_cache.get(key)
    if info is None:
        info = eos.db.gamedata_session.query(col.table).filter(col == value).count() > 0
        existence_cache[key] = info
 
    return info
 
if __name__ == "__main__":
    from ConfigParser import ConfigParser
    from optparse import OptionParser
 
    from reverence import blue
    from sqlalchemy import Boolean
    from sqlalchemy.orm import class_mapper, ColumnProperty
 
    import eos.config
 
    # Parse command line options
    usage = "usage: %prog --eve=EVE --cache=CACHE --dump=DUMP [--release=RELEASE --sisi]"
    parser = OptionParser(usage=usage)
    parser.add_option("-e", "--eve", help="path to eve folder")
    parser.add_option("-c", "--cache", help="path to eve cache folder")
    parser.add_option("-d", "--dump", help="the SQL Alchemy connection string of where we should place our final dump")
    parser.add_option("-r", "--release", help="database release number, defaults to 1", default="1")
    parser.add_option("-s", "--sisi", action="store_true", dest="singularity", help="if you're going to work with Singularity test server data, use this option", default=False)
    (options, args) = parser.parse_args()
 
 
    # Exit if we do not have any of required options
    if not options.eve or not options.cache or not options.dump:
        sys.stderr.write("You need to specify paths to eve folder, cache folder and SQL Alchemy connection string. Run script with --help option for further info.\n")
        sys.exit()
 
    # We can deal either with singularity or tranquility servers
    if options.singularity: server = "singularity"
    else: server = "tranquility"
 
    # Set static variables for paths
    PATH_EVE = os.path.expanduser(options.eve)
    PATH_CACHE = os.path.expanduser(options.cache)
 
    eos.config.gamedata_connectionstring = options.dump
    eos.config.debug = False
 
    from eos.gamedata import *
    import eos.db
 
    # Get version of EVE client
    config = ConfigParser()
    config.read(os.path.join(PATH_EVE, "common.ini"))
 
    # Form metadata dictionary for corresponding table
    metadata = {}
    metadata["version"] = config.getint("main", "build")
    metadata["release"] = options.release
 
    # Initialize Reverence cache manager
    eve = blue.EVE(PATH_EVE, cachepath=PATH_CACHE, server=server)
    cfg = eve.getconfigmgr()
 
    # Create all tables we need
    eos.db.gamedata_meta.create_all()
 
    # Add versioning info to the metadata table
    for fieldname in metadata:
        eos.db.gamedata_session.add(MetaData(fieldname, metadata[fieldname]))
 
    eos.db.gamedata_session.commit()
 
    # Get table map, processing order and special table data
    TABLE_MAP = get_map()
    TABLE_ORDER = get_order()
    CUSTOM_CALLS = get_customcalls()
 
    # Warn about various stuff
    for table in cfg.tables:
        if not table in TABLE_MAP:
            # Warn about new tables in cache which are still not described by table map
            print "Warning: unmapped table {0} found in cache".format(table)
    for table in TABLE_MAP:
        if not table in cfg.tables and not table in CUSTOM_CALLS:
            # Warn about mapped tables which are missing in cache
            print "Warning: mapped table {0} cannot be found in cache".format(table)
        if not table in TABLE_ORDER and TABLE_MAP[table] is not None:
            # Warn about mapped tables not specified in processing order
            print "Warning: mapped table {0} is missing in processing order".format(table)
    for table in TABLE_ORDER:
        if not table in TABLE_MAP:
            # Warn about unmapped tables in processing order
            print "Warning: unmapped table {0} is specified in processing order".format(table)
 
    # Get data from reverence and write it
    for tablename in TABLE_ORDER:
        tableclass = TABLE_MAP[tablename]
        if tableclass is not None:
            # Print currently processed table name
            print "Processing: {0}".format(tablename)
            # Get table object from the Reverence and process it
            source_table = getattr(cfg, tablename) if tablename not in CUSTOM_CALLS else CUSTOM_CALLS[tablename]
            # Gather data regarding columns for current table in cache and eos
            cols_eos = set(prop.key for prop in class_mapper(TABLE_MAP[tablename]).iterate_properties if isinstance(prop, ColumnProperty))
            cols_rev = set(get_source_headers(source_table))
            notineos = cols_rev.difference(cols_eos)
            notinrev = cols_eos.difference(cols_rev)
            if notineos:
                print "Warning: {0} found in cache but missing in eos definitions: {1}".format("column" if len(notineos) == 1 else "columns", ", ".join(sorted(notineos)))
            if notinrev:
                print "Warning: {0} found in eos definitions but missing in cache: {1}".format("column" if len(notinrev) == 1 else "columns", ", ".join(sorted(notinrev)))
            process_table(source_table, tablename, tableclass)