#!/usr/local/bin/python2.7
# -*- coding: utf-8 -*-
#
# (c) Copyright 2003-2008 Hewlett-Packard Development Company, L.P.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# Author: Don Welch, Naga Samrat Chowdary Narla,

# Local
from base.g import *
from base import utils

# StdLib
import os.path
import re
import glob

try:
    import datetime
    datetime_avail = True
except ImportError:
    datetime_avail = False
    datetime = None


pat_prod_num = re.compile("""(\d+)""", re.I)

TYPE_UNKNOWN = 0
TYPE_STRING = 1
TYPE_STR = 1
TYPE_LIST = 2
TYPE_BOOL = 3
TYPE_INT = 4
TYPE_HEX = 5
TYPE_BITFIELD = 6
TYPE_URI = TYPE_STR # (7) not used (yet)
TYPE_DATE = 8  # format: mm/dd/yyyy


TECH_CLASSES = [
    "Undefined", # This will show an error (and its the default)
    "Unsupported", # This is for unsupported models, and it will not show an error
    "Postscript",
    "DJGenericVIP",
    #"PSB9100", not used on HPLIP
    "LJMono",
    "LJColor",
    "LJFastRaster",
    "LJJetReady",
    "DJ350",
    #"DJ400", not used on HPLIP
    "DJ540",
    "DJ600",
    "DJ6xx",
    "DJ6xxPhoto",
    "DJ630",
    #"DJ660", not used in HPLIP
    "DJ8xx",
    "DJ8x5",
    "DJ850",
    "DJ890",
    "DJ9xx",
    "DJ9xxVIP",
    "DJ3600",
    "DJ3320",
    "DJ4100",
    "AP2xxx",
    "AP21xx",
    "AP2560",
    "PSP100",
    "PSP470",
    "LJZjsMono",
    "LJZjsColor",
    "LJm1005",
    "QuickConnect",
    "DJ55xx",
    "OJProKx50",
    'LJP1XXX',
    #'DJD2600', not used. Reassigned all these to ViperPlusTrim and ViperMinusTrim Class
    "Stabler",
    "ViperPlusVIP",
    "ViperMinusVIP",
    "ViperPlusTrim",
    "ViperMinusTrim",
    "Corbett",
    "Python",
    "OJ7000",
    "Pyramid",
    "Python10",
    "Mimas",
    "StingrayOJ",
    "Copperhead",
    "Ampere",
    "Python11",
]

TECH_CLASSES.sort()

TECH_CLASS_PDLS = {
    #"Undefined"    : '?',
    "Postscript"   : 'ps',
    "DJGenericVIP" : 'pcl3',
    #"PSB9100"      : 'pcl3',
    "LJMono"       : 'pcl3',
    "LJColor"      : 'pcl3',
    "LJFastRaster" : 'pclxl',
    "LJJetReady"   : 'pclxl',
    "DJ350"        : 'pcl3',
    #"DJ400"        : 'pcl3',
    "DJ540"        : 'pcl3',
    "DJ600"        : 'pcl3',
    "DJ6xx"        : 'pcl3',
    "DJ6xxPhoto"   : 'pcl3',
    "DJ630"        : 'pcl3',
    #"DJ660"        : 'pcl3',
    "DJ8xx"        : 'pcl3',
    "DJ8x5"        : 'pcl3',
    "DJ850"        : 'pcl3',
    "DJ890"        : 'pcl3',
    "DJ9xx"        : 'pcl3',
    "DJ9xxVIP"     : 'pcl3',
    "DJ3600"       : 'lidil',
    "DJ3320"       : 'lidil',
    "DJ4100"       : 'lidil',
    "AP2xxx"       : 'pcl3',
    "AP21xx"       : 'pcl3',
    "AP2560"       : 'pcl3',
    "PSP100"       : 'pcl3',
    "PSP470"       : 'pcl3',
    "LJZjsMono"    : 'zjs',
    "LJZjsColor"   : 'zjs',
    "LJm1005"      : 'zxs',
    "QuickConnect" : 'jpeg',
    "DJ55xx"       : 'pcl3',
    "OJProKx50"    : 'pcl3',
    'LJP1XXX'      : 'zxs',
    "Stabler"      : 'pcl3',
    "ViperPlusVIP" : 'pcl3',
    "ViperMinusVIP": 'pcl3',
    "ViperPlusTrim" : 'lidil',
    "ViperMinusTrim": 'lidil',
    "Corbett"       : 'pcl3',
    "Python"        : 'pcl3',
    "OJ7000"        : 'pcl3',
    "Python10"      : 'pcl3',
    "Mimas"      : 'pcl3',
    "StingrayOJ"   : 'pcl3',
    "Copperhead"   : 'pcl3',
    "Ampere"	   : 'pcl3'
}

PDL_TYPE_PCL = 0  # less preferred
PDL_TYPE_PS = 1   #      /\
PDL_TYPE_HOST = 2 # more preferred (however, may req. plugin)

PDL_TYPES = { # Used to prioritize PPD file selection in prnt.cups.getPPDFile2()
    'pcl3' : PDL_TYPE_PCL,
    'pcl5' : PDL_TYPE_PCL,
    'pcl6' : PDL_TYPE_PCL,
    'pcl5e' : PDL_TYPE_PCL,
    'pcl' : PDL_TYPE_PCL,
    'pclxl' : PDL_TYPE_PCL,
    'ps' : PDL_TYPE_PS,
    'lidil' : PDL_TYPE_HOST,
    'zjs' : PDL_TYPE_HOST,
    'zjstream' : PDL_TYPE_HOST,
    'zxs' : PDL_TYPE_HOST,
    'zxstream' : PDL_TYPE_HOST,
    'jpeg' : PDL_TYPE_HOST,
    'jpg' : PDL_TYPE_HOST,
    'jetready' : PDL_TYPE_HOST,
    'jr' : PDL_TYPE_HOST,
}


TECH_SUBCLASSES = [
    "LargeFormatSuperB",
    "LargeFormatA3",
    "CoverMedia", # 3425
    "FullBleed",
    "Duplex",
    "Normal",
    "Apollo2000",
    "Apollo2200",
    "Apollo2500",
    "NoPhotoMode",
    "NoPhotoBestHiresModes",
    "No1200dpiNoSensor",
    "NoFullBleed",
    "4x6FullBleed",
    "300dpiOnly",  # LaserJet 4L
    "GrayscaleOnly", # DJ540
    "NoAutoTray", # PS Pro 8850
    "NoEvenDuplex", # PS C8100
    "NoAutoDuplex",
    "NoCDDVD",
    "NoMaxDPI",
    "NoMaxDPI",
    "SmallMargins",
    "Trim",
    "4800x1200dpi",
    "Advanced",
    "AutoDuplex"
]

TECH_SUBCLASSES.sort()


# Items will be capitalized unless in this dict
MODEL_UI_REPLACEMENTS = {'laserjet'   : 'LaserJet',
                          'psc'        : 'PSC',
                          'hp'         : 'HP',
                          'mfp'        : 'MFP',
                        }


def normalizeModelUIName(model):
    ml = model.lower().strip()

    if 'apollo' in ml:
        z = ml.replace('_', ' ')
    else:
        if ml.startswith("hp"):
            z = ml[3:].replace('_', ' ')
        else:
            z = ml.replace('_', ' ')

    y = []
    for x in z.split():
        if pat_prod_num.search(x): # don't cap items like cp1700dn
            y.append(x)
        else:
            y.append(MODEL_UI_REPLACEMENTS.get(x, x.capitalize()))

    if 'apollo' in ml:
        return ' '.join(y)
    else:
        return "HP " + ' '.join(y)


def normalizeModelName(model):
    return utils.xstrip(model.replace(' ', '_').replace('__', '_').replace('~','').replace('/', '_'), '_')


class ModelData:
    def __init__(self, root_path=None):
        if root_path is None:
            self.root_path = prop.models_dir
        else:
            self.root_path = root_path

        self.__cache = {}
        self.reset_includes()
        self.sec = re.compile(r'^\[(.*)\]')
        self.inc = re.compile(r'^\%include (.*)', re.I)
        self.inc_line = re.compile(r'^\%(.*)\%')
        self.eq = re.compile(r'^([^=]+)=(.*)')
        self.date = re.compile(r'^(\d{1,2})/(\d{1,2})/(\d{4,4})')

        files = [(os.path.join(self.root_path, "models.dat"),
                  os.path.join(self.root_path, "unreleased", "unreleased.dat")),
                 (os.path.join(os.getcwd(), 'data', 'models', 'models.dat'),
                  os.path.join(os.getcwd(), 'data', 'models', 'unreleased', 'unreleased.dat'))]

        for self.released_dat, self.unreleased_dat in files:
            if os.path.exists(self.released_dat):
                break

        else:
            self.released_dat, self.unreleased_dat = None, None
            log.error("Unable to locate models.dat file")

        self.FIELD_TYPES = {
            # Static model query data (from models.dat)
            'align-type' : TYPE_INT,
            'clean-type' : TYPE_INT,
            'color-cal-type' : TYPE_INT,
            'copy-type' : TYPE_INT,
            'embedded-server-type' : TYPE_INT,
            'fax-type' : TYPE_INT,
            'fw-download' : TYPE_BOOL,
            'icon' : TYPE_STR,
            'io-mfp-mode' : TYPE_INT,
            'io-mode' : TYPE_INT,
            'io-support' : TYPE_BITFIELD,
            'job-storage' : TYPE_INT,
            'monitor-type' : TYPE_INT,
            'linefeed-cal-type' : TYPE_INT,
            'panel-check-type' : TYPE_INT,
            'pcard-type' : TYPE_INT,
            'plugin' : TYPE_INT,
            'plugin-reason' : TYPE_BITFIELD,
            'power-settings': TYPE_INT,
            'pq-diag-type' : TYPE_INT,
            'r-type' : TYPE_INT,
            'scan-style' : TYPE_INT,
            'scan-type' : TYPE_INT,
            'scan-src' : TYPE_INT,
            #'scan-duplex' : TYPE_BOOL,
            'status-battery-check' : TYPE_INT,
            'status-dynamic-counters' : TYPE_INT,
            'status-type' : TYPE_INT,
            'support-subtype' : TYPE_HEX,
            'support-released' : TYPE_BOOL,
            'support-type' : TYPE_INT,
            'support-ver' : TYPE_STR,
            'tech-class' : TYPE_LIST,
            'tech-subclass' : TYPE_LIST,
            'tech-type' : TYPE_INT,
            'usb-pid' : TYPE_HEX,
            'usb-vid' : TYPE_HEX,
            'wifi-config': TYPE_INT,
            'ppd-name' : TYPE_STR,
            }

        self.FIELD_TYPES_DYN = {
            # Dynamic model data (from device query)
            'dev-file' : TYPE_STR,
            'fax-uri' : TYPE_STR,
            'scan-uri' : TYPE_STR,
            'is-hp' : TYPE_BOOL,
            'host' : TYPE_STR,
            'status-desc' : TYPE_STR,
            'cups-printers' : TYPE_STR,
            'serial' : TYPE_STR,
            'error-state' : TYPE_INT,
            'device-state' : TYPE_INT,
            'panel' : TYPE_INT,
            'device-uri' : TYPE_STR,
            'panel-line1' : TYPE_STR,
            'panel-line2' : TYPE_STR,
            'back-end' : TYPE_STR,
            'port' : TYPE_INT,
            'deviceid' : TYPE_STR,
            'cups-uri' : TYPE_STR,
            'status-code' : TYPE_INT,
            'rs' : TYPE_STR,
            'rr' : TYPE_STR,
            'rg' : TYPE_STR,
            'r' : TYPE_INT,
            'duplexer' : TYPE_INT,
            'supply-door' : TYPE_INT,
            'revision' : TYPE_INT,
            'media-path' : TYPE_INT,
            'top-door' : TYPE_BOOL,
            'photo-tray' : TYPE_BOOL,
            }

        self.RE_FIELD_TYPES = {
            re.compile('^r(\d+)-agent(\d+)-kind', re.IGNORECASE) : TYPE_INT,
            re.compile('^r(\d+)-agent(\d+)-type', re.IGNORECASE) : TYPE_INT,
            re.compile('^r(\d+)-agent(\d+)-sku', re.IGNORECASE) : TYPE_STR,
            re.compile('^agent(\d+)-desc', re.IGNORECASE) : TYPE_STR,
            re.compile('^agent(\d+)-virgin', re.IGNORECASE) : TYPE_BOOL,
            re.compile('^agent(\d+)-dvc', re.IGNORECASE) : TYPE_INT,
            re.compile('^agent(\d+)-kind', re.IGNORECASE) : TYPE_INT,
            re.compile('^agent(\d+)-type', re.IGNORECASE) : TYPE_INT,
            re.compile('^agent(\d+)-id', re.IGNORECASE) : TYPE_INT,
            re.compile('^agent(\d+)-hp-ink', re.IGNORECASE) : TYPE_BOOL,
            re.compile('^agent(\d+)-health-desc', re.IGNORECASE) : TYPE_STR,
            re.compile('^agent(\d+)-health$', re.IGNORECASE) : TYPE_INT,
            re.compile('^agent(\d+)-known', re.IGNORECASE) : TYPE_BOOL,
            re.compile('^agent(\d+)-level', re.IGNORECASE) : TYPE_INT,
            re.compile('^agent(\d+)-ack', re.IGNORECASE) : TYPE_BOOL,
            re.compile('^agent(\d+)-sku', re.IGNORECASE) : TYPE_STR,
            re.compile('^in-tray(\d+)', re.IGNORECASE) : TYPE_BOOL,
            re.compile('^out-tray(\d+)', re.IGNORECASE) : TYPE_BOOL,
            re.compile('^model(\d+)', re.IGNORECASE) : TYPE_STR,
            }

        self.TYPE_CACHE = {}


    def read_all_files(self, unreleased=True):
        if os.path.exists(self.released_dat):
            self.read_section(self.released_dat)

            if self.unreleased_dat is not None and os.path.exists(self.unreleased_dat):
                self.read_section(self.unreleased_dat )

        return self.__cache


    def read_section(self, filename, section=None, is_include=False): # section==None, read all sections
        found, in_section = False, False

        if section is not None:
            section = section.lower()

            if is_include:
                log.debug("Searching for include [%s] in file %s" % (section, filename))
            else:
                log.debug("Searching for section [%s] in file %s" % (section, filename))

        if is_include:
            cache = self.__includes
        else:
            cache = self.__cache

        try:
            fd = file(filename)
        except IOError, e:
            log.error("I/O Error: %s (%s)" % (filename, e.strerror))
            return False

        while True:
            line = fd.readline()

            if not line:
                break

            if line[0] in ('#', ';'):
                continue

            if line[0] == '[':
                if in_section and section is not None:
                    break

                match = self.sec.search(line)

                if match is not None:
                    in_section = True

                    read_section = match.group(1).lower()

                    if section is not None:
                        found = in_section = (read_section == section)

                    if in_section:
                        if section is not None:
                            log.debug("Found section [%s] in file %s" % (read_section, filename))

                        cache[read_section] = {}

                continue

            if line[0] == '%':
                match = self.inc.match(line)

                if match is not None:
                    inc_file = match.group(1)
                    log.debug("Found include file directive: %%include %s" % inc_file)
                    self.__include_files.append(os.path.join(os.path.dirname(filename), inc_file))
                    continue

                if in_section:
                    match = self.inc_line.match(line)

                    if match is not None:
                        inc_sect = match.group(1)
                        log.debug("Found include directive %%%s%%" % inc_sect)

                        try:
                            self.__includes[inc_sect]
                        except KeyError:
                            for inc in self.__include_files:

                                if self.read_section(inc, inc_sect, True):
                                    break
                            else:
                                log.error("Include %%%s%% not found." % inc_sect)

            if in_section:
                match = self.eq.search(line)

                if match is not None:
                    key = match.group(1)
                    value = match.group(2)
                    value = self.convert_data(key, value)
                    cache[read_section][key] = value

        fd.close()
        return found


    def reset_includes(self):
        self.__include_files = []
        self.__includes = {}


    def __getitem__(self, model):
        model = model.lower()

        try:
            return self.__cache[model]
        except:
            log.debug("Cache miss: %s" % model)

            log.debug("Reading file: %s" % self.released_dat)

            if self.read_section(self.released_dat, model):
                return self.__cache[model]

            if self.unreleased_dat is not None and os.path.exists(self.unreleased_dat):
                log.debug("Reading file: %s" % self.unreleased_dat)

                if self.read_section(self.unreleased_dat, model):
                    return self.__cache[model]

            return {}


    def all_models(self):
        return self.__cache


    def get_data_type(self, key):
        try:
            return self.FIELD_TYPES[key]
        except KeyError:
            try:
                return self.FIELD_TYPES_DYN[key]
            except KeyError:
                try:
                    return self.TYPE_CACHE[key]
                except KeyError:
                    for pat, typ in self.RE_FIELD_TYPES.items():
                        match = pat.match(key)
                        if match is not None:
                            self.TYPE_CACHE[key] = typ
                            return typ

        log.error("get_data_type(): Field type lookup failed for key %s" % key)
        return None


    def convert_data(self, key, value, typ=None):
        if typ is None:
            typ = self.get_data_type(key)

        if  typ in (TYPE_BITFIELD, TYPE_INT):
            try:
                value = int(value)
            except (ValueError, TypeError):
                log.error("Invalid value in .dat file: %s=%s" % (key, value))
                value = 0

        elif typ == TYPE_BOOL:
            value = utils.to_bool(value)

        elif typ == TYPE_LIST:
            value = [x for x in value.split(',') if x]

        elif typ == TYPE_DATE: # mm/dd/yyyy
            if datetime_avail:
                # ...don't use datetime.strptime(), wasn't avail. until 2.5
                match = self.date.search(value)

                if match is not None:
                    month = int(match.group(1))
                    day = int(match.group(2))
                    year = int(match.group(3))

                    value = datetime.date(year, month, day)

        elif typ == TYPE_HEX:
            try:
                value = int(value, 16)
            except (ValueError, TypeError):
                log.error("Invalid hex value in .dat file: %s=%s" % (key, value))
                value = 0

        return value
