from __future__ import absolute_import
import nilmdb
from nilmdb.printf import *

import tables
import time
import sys
import inspect
import cStringIO

class ParserError(Exception):
    def __init__(self, line, message):
        self.message = sprintf("line %d: %s", line, message)
        Exception.__init__(self, self.message)

class Layout(object):
    """Represents a NILM database layout"""
    def description(self):
        """Return the PyTables description of this layout"""
        desc = {}
        for (n, (name, type)) in enumerate(self.fields):
            desc[name] = tables.Col.from_type(type, pos=n+1)
        return tables.Description(desc)

    def parse(self, inp):
        """Given inp as text, return a list of values
        converted to the correct types"""
        # Consider overriding this in subclasses for speed.
        # In general it takes about 2/3 the time that way.
        out = []
        for (n, (name, type)) in enumerate(self.fields):
            if name == 'timestamp':
                out.append(int(float(inp[n]) * 1e6))
            elif type == 'float32':
                out.append(float(inp[n]))
            elif type == 'uint16':
                x = int(inp[n], 10)
                if x < 0 or x > 65535:
                    raise ValueError("data out of range")
                out.append(x)
            else:
                raise TypeError("can't parse type " + repr(type))
        return out

def parse_timestamp(string):
    return int(float(string) * 1e6)

class PrepData(Layout):
    rate_hz = 120
    fields = [ ( 'timestamp', 'int64' ),
               ( 'p1', 'float32' ),
               ( 'q1', 'float32' ),
               ( 'p3', 'float32' ),
               ( 'q3', 'float32' ),
               ( 'p5', 'float32' ),
               ( 'q5', 'float32' ),
               ( 'p7', 'float32' ),
               ( 'q7', 'float32' ) ]
    def parse(self, inp):
        return [ parse_timestamp(inp[0]),
                 float(inp[1]), float(inp[2]),
                 float(inp[3]), float(inp[4]),
                 float(inp[5]), float(inp[6]),
                 float(inp[7]), float(inp[8]) ]

class RawData(Layout):
    rate_hz = 8000
    fields = [ ( 'timestamp', 'int64' ),
               ( 'va', 'uint16' ),
               ( 'vb', 'uint16' ),
               ( 'vc', 'uint16' ),
               ( 'ia', 'uint16' ),
               ( 'ib', 'uint16' ),
               ( 'ic', 'uint16' ) ]

class RawNotchedData(RawData):
    rate_hz = 8000
    fields = RawData.fields + [
        ( 'notch_ia', 'uint16' ),
        ( 'notch_ib', 'uint16' ),
        ( 'notch_ic', 'uint16' ) ]

# Instantiate all layouts, indexed by their name
named = {}
for name, obj in inspect.getmembers(sys.modules[__name__]):
    if inspect.isclass(obj) and issubclass(obj, Layout):
        named[name] = obj()

class Parser(object):
    """Object that parses and stores ASCII data for inclusion into the database"""
    def __init__(self, layout):
        if issubclass(layout.__class__, Layout):
            self.layout = layout
        else:
            try:
                self.layout = named[layout]
            except KeyError:
                raise TypeError("unknown layout")

        self.data = []
        self.min_timestamp = None
        self.max_timestamp = None
        # Assume timestamp is always the first field, for now
        self.ts_field = 0

    def parse(self, textdata):
        """Parse the data, provided as lines of text, using the current
        layout, into an internal data structure."""

        indata = cStringIO.StringIO(textdata)
        n = 0
        # Assume any parsing error is a real error.
        # In the future we might want to skip completely empty lines,
        # or partial lines right before EOF?
        try:
            last_ts = None
            for line in indata:
                n += 1

                # Parse and append
                fields = line.partition('#')[0].split()
                if len(fields) != len(self.layout.fields):
                    raise IndexError(sprintf("wanted %d fields, got %d",
                                             len(self.layout.fields), len(fields)))
                out = self.layout.parse(fields)
                self.data.append(out)

                # Verify timestamp
                if self.ts_field is not None:
                    if last_ts is not None and out[self.ts_field] < last_ts:
                        raise ValueError("timestamp is not monotonically increasing")
                    last_ts = out[self.ts_field]
        except ValueError as e:
            raise ParserError(n, "value error: " + e.message)
        except IndexError as e:
            raise ParserError(n, "index error: " + e.message)
        except TypeError as e:
            raise ParserError(n, "type error: " + e.message)

        # Mark timestamp ranges
        if len(self.data) and self.ts_field is not None:
            self.min_timestamp = self.data[0][self.ts_field]
            self.max_timestamp = self.data[-1][self.ts_field]
