#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2020 SkyWater PDK Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0


import argparse
import enum
import json
import os
import pathlib
import pprint
import re
import sys

from collections import defaultdict

from typing import Tuple, List, Dict

from . import sizes
from .utils import sortable_extracted_numbers


debug = False


class TimingType(enum.IntFlag):
    """

    >>> TimingType.parse("ff_100C_1v65")
    ('ff_100C_1v65', <TimingType.basic: 1>)

    >>> TimingType.parse("ff_100C_1v65_ccsnoise")
    ('ff_100C_1v65', <TimingType.ccsnoise: 3>)

    >>> TimingType.basic in TimingType.ccsnoise
    True

    >>> TimingType.parse("ff_100C_1v65_pwrlkg")
    ('ff_100C_1v65', <TimingType.leakage: 4>)

    >>> (TimingType.basic).describe()
    ''
    >>> (TimingType.ccsnoise).describe()
    '(with ccsnoise)'
    >>> (TimingType.leakage).describe()
    '(with power leakage)'
    >>> (TimingType.leakage | TimingType.ccsnoise).describe()
    '(with ccsnoise and power leakage)'

    >>> (TimingType.leakage | TimingType.ccsnoise).names()
    'basic, ccsnoise, leakage'

    >>> TimingType.ccsnoise.names()
    'basic, ccsnoise'
    """

    basic    = 1

    # ccsnoise files are basic files with extra 'ccsn_' values in the timing
    # data.
    ccsnoise = 2 | basic

    # leakage files are separate from the basic files
    leakage  = 4

    def names(self):
        o = []
        for t in TimingType:
            if t in self:
                o.append(t.name)
        return ", ".join(o)

    def describe(self):
        o = []
        if TimingType.ccsnoise in self:
            o.append("ccsnoise")
        if TimingType.leakage in self:
            o.append("power leakage")
        if not o:
            return ""
        return "(with "+" and ".join(o)+")"

    @property
    def file(self):
        if self == TimingType.ccsnoise:
            return "_ccsnoise"
        elif self == TimingType.leakage:
            return "_pwrlkg"
        return ""

    @classmethod
    def parse(cls, name):
        ttype = TimingType.basic
        if name.endswith("_ccsnoise"):
            name = name[:-len("_ccsnoise")]
            ttype = TimingType.ccsnoise
        elif name.endswith("_pwrlkg"):
            name = name[:-len("_pwrlkg")]
            ttype = TimingType.leakage
        return name, ttype

    @property
    def singular(self):
        return len(self.types) == 1

    @property
    def types(self):
        tt = set(t for t in TimingType if t in self)
        if TimingType.ccsnoise in tt:
            tt.remove(TimingType.basic)
        return list(tt)



def cell_corner_file(lib, cell_with_size, corner, corner_type: TimingType):
    """

    >>> cell_corner_file("sky130_fd_sc_hd", "a2111o", "ff_100C_1v65", TimingType.basic)
    'cells/a2111o/sky130_fd_sc_hd__a2111o__ff_100C_1v65.lib.json'
    >>> cell_corner_file("sky130_fd_sc_hd", "a2111o_1", "ff_100C_1v65", TimingType.basic)
    'cells/a2111o/sky130_fd_sc_hd__a2111o_1__ff_100C_1v65.lib.json'
    >>> cell_corner_file("sky130_fd_sc_hd", "a2111o_1", "ff_100C_1v65", TimingType.ccsnoise)
    'cells/a2111o/sky130_fd_sc_hd__a2111o_1__ff_100C_1v65_ccsnoise.lib.json'

    """
    assert corner_type.singular, (lib, cell_with_size, corner, corner_type, corner_type.types())

    sz = sizes.parse_size(cell_with_size)
    if sz:
        cell = cell_with_size[:-len(sz.suffix)]
    else:
        cell = cell_with_size

    fname = "cells/{cell}/{lib}__{cell_sz}__{corner}{corner_type}.lib.json".format(
        lib=lib, cell=cell, cell_sz=cell_with_size, corner=corner, corner_type=corner_type.file)
    return fname


def top_corner_file(libname, corner, corner_type: TimingType):
    """

    >>> top_corner_file("sky130_fd_sc_hd", "ff_100C_1v65", TimingType.ccsnoise)
    'timing/sky130_fd_sc_hd__ff_100C_1v65_ccsnoise.lib.json'
    >>> top_corner_file("sky130_fd_sc_hd", "ff_100C_1v65", TimingType.basic)
    'timing/sky130_fd_sc_hd__ff_100C_1v65.lib.json'

    """
    assert corner_type.singular, (libname, corner, corner_type, corner_type.types())
    return "timing/{libname}__{corner}{corner_type}.lib.json".format(
        libname=libname,
        corner=corner, corner_type=corner_type.file)


def collect(library_dir) -> Tuple[Dict[str, TimingType], List[str]]:
    """Collect the available timing information in corners.

    Parameters
    ----------
    library_dir: str
        Path to a library.

    Returns
    -------
    lib : str
        Library name

    corners : {str: TimingType}
        corners in the library.

    cells : list of str
        cells in the library.
    """

    if not isinstance(library_dir, pathlib.Path):
        library_dir = pathlib.Path(library_dir)

    libname0 = None

    corners = {}
    all_cells = set()
    for p in library_dir.rglob("*.lib.json"):
        if not p.is_file():
            continue
        if "timing" in str(p):
            continue

        fname, fext = str(p.name).split('.', 1)

        libname, cellname, corner = fname.split("__")
        if libname0 is None:
            libname0 = libname
        assert libname0 == libname, (libname0, libname)

        corner_name, corner_type = TimingType.parse(corner)

        if corner_name not in corners:
            corners[corner_name] = [corner_type, set()]

        corners[corner_name][0] |= corner_type
        corners[corner_name][1].add(cellname)
        all_cells.add(cellname)

    for c in corners:
        corners[c] = (corners[c][0], list(sorted(corners[c][1])))

    assert corners, library_dir
    assert all_cells, library_dir
    assert libname0, library_dir

    all_cells = list(sorted(all_cells))

    # Sanity check to make sure the corner exists for all cells.
    for corner, (corner_types, corner_cells) in sorted(corners.items()):
        missing = set()
        for cell_with_size in all_cells:
            if cell_with_size not in corner_cells:
                missing.add(cell_with_size)

        if not missing:
            continue

        print("Missing", ", ".join(missing), "from", corner, corner_types)

    return libname0, corners, all_cells

    for corner, (corner_types, corner_cells) in sorted(corners.items()):
        for corner_type in corner_types.types:
            fname = cell_corner_file(libname0, cell_with_size, corner, corner_type)
            fpath = os.path.join(library_dir, fname)
            if not os.path.exists(fpath) and debug:
                print("Missing", (fpath, corner, corner_type, corner_types))

    timing_dir = os.path.join(library_dir, "timing")
    assert os.path.exists(timing_dir), timing_dir
    for corner, (corner_types, corner_cells) in sorted(corners.items()):
        for corner_type in corner_types.types:
            fname = top_corner_file(libname0, corner, corner_type)
            fpath = os.path.join(library_dir, fname)
            if not os.path.exists(fpath) and debug:
                print("Missing", (fpath, corner, corner_type, corner_types))

    return libname0, corners, all_cells


def remove_ccsnoise_from_timing(data, dataname):
    assert "timing" in data, (dataname, data.keys(), data)

    timing = data["timing"]

    if isinstance(timing, list):
        for i, t in enumerate(timing):
            assert isinstance(t, dict), (dataname, i, t)
            remove_ccsnoise_from_dict(t, "{}.timing[{:3d}]".format(dataname, i))
    elif isinstance(timing, dict):
        remove_ccsnoise_from_dict(timing, dataname+".timing")
    else:
        assert False, (dataname, type(timing), timing)


def remove_ccsnoise_from_dict(data, dataname):
    if "timing" in data:
        remove_ccsnoise_from_timing(data, dataname)

    ccsn_keys = set()
    for k in data:
        if "ccsn_" in k:
            ccsn_keys.add(k)

    for k in ccsn_keys:
        if debug:
            print("{:s}: Removing {}".format(dataname, k))
        del data[k]



def remove_ccsnoise_from_cell(data, cellname):
    remove_ccsnoise_from_dict(data, cellname)

    for k, v in list(data.items()):
        if k.startswith("pin "):
            pin_data = data[k]
            if "input_voltage" in pin_data:
                del pin_data["input_voltage"]

            remove_ccsnoise_from_dict(pin_data, "{}.{}".format(cellname, k))

        if k.startswith("bus"):
            bus_data = data[k]
            remove_ccsnoise_from_dict(bus_data, "{}.{}".format(cellname, k))


remove_ccsnoise_from_library = remove_ccsnoise_from_dict


def generate(library_dir, lib, corner, ocorner_type, icorner_type, cells):
    top_fname = top_corner_file(lib, corner, ocorner_type).replace('.lib.json', '.lib')
    top_fpath = os.path.join(library_dir, top_fname)

    top_fout = open(top_fpath, "w")
    def top_write(lines):
        print("\n".join(lines), file=top_fout)

    otype_str = "({} from {})".format(ocorner_type.name, icorner_type.names())
    print("Starting to write", top_fpath, otype_str, flush=True)

    common_data = {}

    common_data_path = os.path.join(library_dir, "timing", "{}__common.lib.json".format(lib))
    assert os.path.exists(common_data_path), common_data_path
    with open(common_data_path) as f:
        d = json.load(f)
        assert isinstance(d, dict)
        for k, v in d.items():
            assert k not in common_data, (k, common_data[k])
            common_data[k] = v

    top_data_path = os.path.join(library_dir, top_corner_file(lib, corner, icorner_type))
    assert os.path.exists(top_data_path), top_data_path
    with open(top_data_path) as f:
        d = json.load(f)
        assert isinstance(d, dict)
        for k, v in d.items():
            if k in common_data:
                print("Overwriting", k, "with", v, "(existing value of", common_data[k], ")")
            common_data[k] = v

    # Remove the ccsnoise if it exists
    if ocorner_type != TimingType.ccsnoise:
        remove_ccsnoise_from_library(common_data, "library")

    attribute_types = {}
    output = liberty_dict("library", lib+"__"+corner, common_data, attribute_types)
    assert output[-1] == '}', output
    top_write(output[:-1])

    for cell_with_size in cells:
        fname = cell_corner_file(lib, cell_with_size, corner, icorner_type)
        fpath = os.path.join(library_dir, fname)
        assert os.path.exists(fpath), fpath

        with open(fpath) as f:
            cell_data = json.load(f)

        # Remove the ccsnoise if it exists
        if ocorner_type != TimingType.ccsnoise:
            remove_ccsnoise_from_cell(cell_data, cell_with_size)

        top_write([''])
        top_write(liberty_dict(
            "cell",
            "%s__%s" % (lib, cell_with_size),
            cell_data,
            [cell_with_size],
            attribute_types,
        ))

    top_write([''])
    top_write(['}'])
    top_fout.close()
    print("   Finish writing", top_fpath, flush=True)
    print("")


# * The 'delay_model' should be the 1st attribute in the library
# * The 'technology' should be the 1st attribute in the library

LIBERTY_ATTRIBUTE_ORDER = re.sub('/\\*[^*]*\\*/', '', """
library (name_string) {
    /* Library-Level Simple and Complex Attributes */
    define (...,...,...) ;
    technology (name_enum) ;
    delay_model : "model" ;

    bus_naming_style : "string" ;
    date : "date" ;
    comment : "string" ;

    /* Unit definitions */
    time_unit : "unit" ;
    voltage_unit : "unit" ;
    leakage_power_unit : "unit" ;
    current_unit : "unit" ;
    pulling_resistance_unit : "unit" ;
    ..._unit : "unit" ;
    /* FIXME: Should capacitive_load_unit always be last? */
    capacitive_load_unit (value, unit) ;

    /* FIXME: Why is define_cell_area here, while other defines are up above? */
    define_cell_area (area_name, resource_type) ;

    revision : float | string ;

    /* Default Attributes and Values */
    default_cell_leakage_power : float ;
    default_fanout_load : float ;
    default_inout_pin_cap : float ;
    default_input_pin_cap : float ;
    default_max_transition : float ;
    default_output_pin_cap : float ;
    default_... : ... ;

    /* Scaling Factors Attributes and Values */
    k_process_cell_fall ... ;
    k_process_cell_rise ... ;
    k_process_fall_propagation ... ;
    k_process_fall_transition ... ;
    k_process_rise_propagation ... ;
    k_process_rise_transition ... ;
    k_temp_cell_fall ... ;
    k_temp_cell_rise ... ;
    k_temp_fall_propagation ... ;
    k_temp_fall_transition ... ;
    k_temp_rise_propagation ... ;
    k_temp_rise_transition ... ;
    k_volt_cell_fall ... ;
    k_volt_cell_rise ... ;
    k_volt_fall_propagation ... ;
    k_volt_fall_transition ... ;
    k_volt_rise_propagation ... ;
    k_volt_rise_transition ... ;
    k_... : ... ;

    /* Library-Level Group Statements */
    operating_conditions (name_string) {
        ... operating conditions description ...
    }
    wire_load (name_string) {
        ... wire load description ...
    }
    wire_load_selection (name_string) {
        ... wire load selection criteria...
    }
    power_lut_template (namestring)  {
        ... power lookup table template information...
    }
    lu_table_template (name_string) {
        variable_1 : value_enum ;
        variable_2 : value_enum ;
        variable_3 : value_enum ;
        index_1 ("float, ..., float");
        index_2 ("float, ..., float");
        index_3 ("float, ..., float");
    }
    normalized_driver_waveform (waveform_template_name) {
        driver_waveform_name : string; /* Specifies the name of the driver waveform table */
        index_1 ("float, ... float"); /* Specifies input net transition */
        index_2 ("float, ... float"); /* Specifies normalized voltage */
        values ("float, ... float", \ /* Specifies the time in library units */
            ... , \\
            "float, ... float");
    }

    /* Cell definitions */
    cell (namestring2) {
        ... cell description ...
    }

    ...

    /* FIXME: What are these and why are they last */
    type (namestring) {
        ... type description ...
    }
    input_voltage (name_string) {
        ... input voltage information ...
    }
    output_voltage (name_string) {
        ... output voltage information ...
    }
}
""")


RE_LIBERTY_LIST = re.compile("(.*)_([0-9]+)")
RE_NUMBERS = re.compile('([0-9]+)')


def _lookup_attribute_pos(name):
    # Pad with spaces so you don't get substring matches.
    name = ' ' + name
    if name.endswith('_'):
        name = name + ' '
    i = LIBERTY_ATTRIBUTE_ORDER.find(name)
    if i != -1:
        return float(i)
    return None


def liberty_attribute_order(attr_name):
    """

    FIXME: Make these doctests less fragile...
    >>> liberty_attribute_order("define")
    (33.0, 0.0)

    >>> liberty_attribute_order('voltage_map')
    (inf, inf)

    >>> liberty_attribute_order('slew_lower_threshold_pct_fall')
    (inf, inf)

    >>> liberty_attribute_order('time_unit')
    (203.0, 0.0)
    >>> liberty_attribute_order('random_unit')
    (357.0, 0.0)
    >>> liberty_attribute_order('capacitive_load_unit')
    (386.0, 0.0)

    >>> liberty_attribute_order('technology')
    (60.0, 0.0)
    >>> liberty_attribute_order('technology("cmos")')
    (60.0, 0.0)

    >>> liberty_attribute_order('delay_model')
    (89.0, 0.0)

    >>> liberty_attribute_order("cell")
    (2282.0, 0.0)

    >>> v1, v2 = "variable_1", "variable_2"
    >>> i1, i2, i3, i4 = "index_1", "index_2", "index_3", "index_4"
    >>> print('\\n'.join(sorted([v2, i1, v1, i2, i3, i4], key=liberty_attribute_order)))
    variable_1
    variable_2
    index_1
    index_2
    index_3
    index_4

    >>> liberty_attribute_order("values")
    (2182.0, 0.0)

    >>> print('\\n'.join(sorted([
    ...     'default_inout_pin_cap',
    ...     'k_XXXX',
    ...     'k_temp_cell_fall',
    ...     'default_XXXX',
    ... ], key=liberty_attribute_order)))
    default_inout_pin_cap
    default_XXXX
    k_temp_cell_fall
    k_XXXX


    """
    assert ':' not in attr_name, attr_name

    m = RE_LIBERTY_LIST.match(attr_name)
    if m:
        k, n = m.group(1), m.group(2)

        i = _lookup_attribute_pos(k)
        if not i:
            i = float('inf')

        return float(i), float(n)

    lookup_name = attr_name
    i = _lookup_attribute_pos(lookup_name)
    if i:
        return i, 0.0

    if '(' in lookup_name:
        lookup_name = lookup_name[:lookup_name.index('(')]

    if 'default_' in attr_name:
        lookup_name = 'default_...'
    if '_unit' in attr_name:
        lookup_name = '..._unit'
    if 'k_' in attr_name:
        lookup_name = 'k_...'

    i = _lookup_attribute_pos(lookup_name)
    if i:
        return i, 0.0

    return float('inf'), float('inf')


def is_liberty_list(k):
    """

    >>> is_liberty_list("variable_1")
    True
    >>> is_liberty_list("index_3")
    True
    >>> is_liberty_list("values")
    True
    """
    m = RE_LIBERTY_LIST.match(k)
    if m:
        k, n = m.group(1), m.group(2)

    return k in ('variable', 'index', 'values')


def liberty_guess(o):
    """

    >>> liberty_guess('hello')  # doctest: +ELLIPSIS
    <function liberty_str at ...>
    >>> liberty_guess(1.0)      # doctest: +ELLIPSIS
    <function liberty_float at ...>
    >>> liberty_guess(1)        # doctest: +ELLIPSIS
    <function liberty_float at ...>
    >>> liberty_guess(None)
    Traceback (most recent call last):
        ...
    ValueError: None has unguessable type: <class 'NoneType'>

    """
    if isinstance(o, str):
        return liberty_str
    elif isinstance(o, (float,int)):
        return liberty_float
    else:
        raise ValueError("%r has unguessable type: %s" % (o, type(o)))


def liberty_bool(b):
    """

    >>> liberty_bool(True)
    'true'
    >>> liberty_bool(False)
    'false'
    >>> liberty_bool(1.0)
    'true'
    >>> liberty_bool(1.5)
    Traceback (most recent call last):
        ...
    ValueError: 1.5 is not a bool

    >>> liberty_bool(0.0)
    'false'
    >>> liberty_bool(0)
    'false'
    >>> liberty_bool(1)
    'true'
    >>> liberty_bool("error")
    Traceback (most recent call last):
        ...
    ValueError: 'error' is not a bool

    """
    try:
        b2 = bool(b)
    except ValueError:
        b2 = None

    if b2 != b:
        raise ValueError("%r is not a bool" % b)

    return {True: 'true', False: 'false'}[b]


def liberty_str(s):
    """

    >>> liberty_str("hello")
    '"hello"'

    >>> liberty_str('he"llo')
    Traceback (most recent call last):
        ...
    ValueError: '"' is not allow in the string: 'he"llo'

    >>> liberty_str(1.0)
    '"1.0000000000"'

    >>> liberty_str(1)
    '"1.0000000000"'

    >>> liberty_str([])
    Traceback (most recent call last):
        ...
    ValueError: [] is not a string

    >>> liberty_str(True)
    Traceback (most recent call last):
        ...
    ValueError: True is not a string

    """
    try:
        if isinstance(s, (int, float)):
            s = liberty_float(s)
    except ValueError:
        pass

    if not isinstance(s, str):
        raise ValueError("%r is not a string" % s)

    if '"' in s:
        raise ValueError("'\"' is not allow in the string: %r" % s)

    return '"'+s+'"'


def liberty_int(f):
    """

    >>> liberty_int(1.0)
    1
    >>> liberty_int(1.5)
    Traceback (most recent call last):
        ...
    ValueError: 1.5 is not an int

    >>> liberty_int("error")
    Traceback (most recent call last):
        ...
    ValueError: 'error' is not an int

    """
    try:
        f2 = int(f)
    except ValueError as e:
        f2 = None

    if f2 is None or f2 != f:
        raise ValueError("%r is not an int" % f)
    return int(f)


def liberty_float(f):
    """

    >>> liberty_float(1.9208818e-02)
    '0.0192088180'

    >>> liberty_float(1.5)
    '1.5000000000'

    >>> liberty_float(1e20)
    '1.000000e+20'

    >>> liberty_float(1)
    '1.0000000000'

    >>> liberty_float(True)
    Traceback (most recent call last):
        ...
    ValueError: True is not a float

    >>> liberty_float(False)
    Traceback (most recent call last):
        ...
    ValueError: False is not a float

    >>> liberty_float(0)
    '0.0000000000'

    >>> liberty_float(None)
    Traceback (most recent call last):
        ...
    ValueError: None is not a float

    >>> liberty_float('hello')
    Traceback (most recent call last):
        ...
    ValueError: 'hello' is not a float


    """
    try:
        f2 = float(f)
    except (ValueError, TypeError):
        f2 = None

    if isinstance(f, bool):
        f2 = None

    if f is None or f2 != f:
        raise ValueError("%r is not a float" % f)

    WIDTH = len(str(0.0083333333))

    s = json.dumps(f)
    if 'e' in s:
        a, b = s.split('e')
        if '.' not in a:
            a += '.'
        while len(a)+len(b)+1 < WIDTH:
            a += '0'
        s = "%se%s" % (a, b)
    elif '.' in s:
        while len(s) < WIDTH:
            s += '0'
    else:
        if len(s) < WIDTH:
            s += '.'
        while len(s) < WIDTH:
            s += '0'
    return s


LIBERTY_ATTRIBUTE_TYPES = {
    'boolean':  liberty_bool,
    'string':   liberty_str,
    'int':      liberty_int,
    'float':    liberty_float,
}


INDENT="    "


def liberty_composite(k, v, i=tuple()):
    """

    >>> def pl(l):
    ...     print("\\n".join(l))

    >>> pl(liberty_composite("capacitive_load_unit", [1.0, "pf"], []))
    capacitive_load_unit(1.0000000000, "pf");

    >>> pl(liberty_composite("voltage_map", [("vpwr", 1.95), ("vss", 0.0)], []))
    voltage_map("vpwr", 1.9500000000);
    voltage_map("vss", 0.0000000000);

    >>> pl(liberty_composite("library_features", 'report_delay_calculation', []))
    library_features("report_delay_calculation");

    """
    if isinstance(v, tuple):
        v = list(v)
    if not isinstance(v, list):
        v = [v]
    #assert isinstance(v, list), (k, v)

    if isinstance(v[0], (list, tuple)):
        o = []
        for j, l in enumerate(v):
            o.extend(liberty_composite(k, l, i))
        return o

    o = []
    for l in v:
        if isinstance(l, (float, int)):
            o.append(liberty_float(l))
        elif isinstance(l, str):
            assert '"' not in l, (k, v)
            o.append('"%s"' % l)
        else:
            raise ValueError("%s - %r (%r)" % (k, l, v))

    return ["%s%s(%s);" % (INDENT*len(i), k, ", ".join(o))]


def liberty_join(l):
    """

    >>> l = [5, 1.0, 10]
    >>> liberty_join(l)(l)
    '5.0000000000, 1.0000000000, 10.000000000'

    >>> l = [1, 5, 8]
    >>> liberty_join(l)(l)
    '1, 5, 8'

    """
    d = defaultdict(lambda: 0)

    for i in l:
        d[type(i)] += 1

    def types(l):
        return [(i, type(i)) for i in l]

    if d[float] > 0:
        assert (d[float]+d[int]) == len(l), (d, types(l))
        def join(l):
            return ", ".join(liberty_float(f) for f in l)
        return join

    elif d[int] > 0:
        assert d[int] == len(l), (d, types(l))
        def join(l):
            return ", ".join(str(f) for f in l)
        return join

    raise ValueError("Invalid value: %r" % types(l))


def liberty_list(k, v, i=tuple()):
    o = []
    if isinstance(v[0], list):
        o.append('%s%s(' % (INDENT*len(i), k))
        join = liberty_join(v[0])
        for l in v:
            o.append('%s"%s", \\' % (INDENT*(len(i)+1), join(l)))

        o0 = o.pop(0)
        o[0] = o0+o[0].lstrip()

        o[-1] = o[-1][:-3] + ');'
    else:
        join = liberty_join(v)
        o.append('%s%s("%s");' % (INDENT*len(i), k, join(v)))

    return o


def liberty_dict(dtype, dvalue, data, indent=tuple(), attribute_types=None):
    """

    >>> def g(a, b, c):
    ...     return {"group_name": a, "attribute_name":b, "attribute_type": c}
    >>> d = {'float': 1.0, "str": "str"}
    >>> print('\\n'.join(liberty_dict("library", "test", d)))
    library ("test") {
        float : 1.0000000000;
        str : "str";
    }
    >>> d['define'] = [g("cell", "float", "string")]
    >>> print('\\n'.join(liberty_dict("library", "test", d)))
    library ("test") {
        define(float,cell,string);
        float : 1.0000000000;
        str : "str";
    }
    >>> d['define'] = [g("library", "float", "string")]
    >>> print('\\n'.join(liberty_dict("library", "test", d)))
    library ("test") {
        define(float,library,string);
        float : "1.0000000000";
        str : "str";
    }

    """
    assert isinstance(data, dict), (dtype, dvalue, data)

    if attribute_types is None:
        attribute_types = {}
    assert isinstance(attribute_types, dict), (dtype, dvalue, attribute_types)

    o = []

    if dvalue:
        dbits = dvalue.split(",")
        for j, d in enumerate(dbits):
            if '"' in d:
                assert d.startswith('"'), (dvalue, dbits, indent)
                assert d.endswith('"'), (dvalue, dbits, indent)
                dbits[j] = d[1:-1]
        dvalue = ','.join('"%s"' % d.strip() for d in dbits)
    o.append('%s%s (%s) {' % (INDENT*len(indent), dtype, dvalue))

    # Sort the attributes
    def attr_sort_key(item):
        k, v = item

        if "," in k:
            ktype, kvalue = k.split(",", 1)
            sortable_kv = sortable_extracted_numbers(kvalue)
        else:
            ktype = k
            kvalue = ""
            sortable_kv = tuple()

        if ktype == "comp_attribute":
            sortable_kt = liberty_attribute_order(kvalue)
        else:
            sortable_kt = liberty_attribute_order(ktype)

        return sortable_kt, ktype, sortable_kv, kvalue, k, v

    di = [attr_sort_key(i) for i in data.items()]
    di.sort()
    if debug:
        print(" "*len(str(indent)), "s1   s2     ", "%-40s" % "ktype", '%-40r' % "kvalue", "value")
        print("-"*len(str(indent)), "---- ----   ", "-"*40, "-"*40, "-"*44)
        for sk, kt, skv, kv, k, v in di:
            print(str(indent), "%4.0f %4.0f --" % sk, "%-40s" % kt, '%-40r' % kv, end=" ")
            sv = str(v)
            print(sv[:40], end=" ")
            if len(sv) > 40:
                print('...', end=" ")
            print()


    # Output all the attributes
    if dtype not in attribute_types:
        dtype_attribute_types = {}
        attribute_types[dtype] = dtype_attribute_types
    dtype_attribute_types = attribute_types[dtype]

    for _, ktype, _, kvalue, k, v in di:
        indent_n = list(indent)+[k]

        if ktype == 'define':
            for d in sorted(data['define'], key=lambda d: d['group_name']+'.'+d['attribute_name']):

                aname = d['attribute_name']
                gname = d['group_name']
                atype = d['attribute_type']

                o.append('%sdefine(%s,%s,%s);' % (
                    INDENT*len(indent_n), aname, gname, atype))

                assert atype in LIBERTY_ATTRIBUTE_TYPES, (atype, d)
                if gname not in attribute_types:
                    attribute_types[gname] = {}
                attribute_types[gname][aname] = LIBERTY_ATTRIBUTE_TYPES[atype]

        elif ktype == "comp_attribute":
            o.extend(liberty_composite(kvalue, v, indent_n))

        elif isinstance(v, dict):
            assert isinstance(v, dict), (dtype, dvalue, k, v)
            o.extend(liberty_dict(ktype, kvalue, v, indent_n, attribute_types))

        elif isinstance(v, list):
            assert len(v) > 0, (dtype, dvalue, k, v)
            if isinstance(v[0], dict):
                def sk(o):
                    return o.items()

                for l in sorted(v, key=sk):
                    o.extend(liberty_dict(ktype, kvalue, l, indent_n, attribute_types))

            elif is_liberty_list(ktype):
                o.extend(liberty_list(ktype, v, indent_n))

            elif "table" == ktype:
                o.append('%s%s : "%s";' % (INDENT*len(indent_n), k, ",".join(v)))

            elif "clk_width" == ktype:
                for l in sorted(v):
                    o.append('%s%s : "%s";' % (INDENT*len(indent_n), k, l))

            else:
                raise ValueError("Unknown %s: %r\n%s" % ((ktype, kvalue, k), v, indent_n))

        else:
            if ktype in dtype_attribute_types:
                liberty_out = dtype_attribute_types[ktype]
            else:
                liberty_out = liberty_guess(v)
            ov = liberty_out(v)
            o.append("%s%s : %s;" % (INDENT*len(indent_n), k, ov))

    o.append("%s}" % (INDENT*len(indent)))
    return o




def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
            "library_path",
            help="Path to the library.",
            type=pathlib.Path,
            nargs=1)
    parser.add_argument(
            "corner",
            help="Corner to write output for.",
            default=None,
            nargs='*')

    parser.add_argument(
            "--ccsnoise",
            help="Include ccsnoise in file output.",
            action='store_true',
            default=False)
    parser.add_argument(
            "--leakage",
            help="Include power leakage in file output.",
            action='store_true',
            default=False)
    parser.add_argument(
            "--debug",
            help="Include verbose debug output on the console.",
            action='store_true',
            default=False)

    args = parser.parse_args()
    if args.debug:
        global debug
        debug = True

    libdir = args.library_path[0]

    retcode = 0

    lib, corners, all_cells = collect(libdir)

    if args.ccsnoise:
        output_corner_type = TimingType.ccsnoise
    elif args.leakage:
        output_corner_type = TimingType.leakage
    else:
        output_corner_type = TimingType.basic

    if args.corner == ['all']:
        args.corner = list(sorted(k for k, (v0, v1) in corners.items() if output_corner_type in v0))

    if args.corner:
        for acorner in args.corner:
            if acorner in corners:
                continue
            print()
            print("Unknown corner:", acorner)
            retcode = 1
        if retcode != 0:
            args.corner.clear()

    if not args.corner:
        print()
        print("Available corners for", lib+":")
        for k, v in sorted(corners.items()):
            print("  -", k, v[0].describe())
        print()
        return retcode

    print("Generating", output_corner_type.name, "liberty timing files for", lib, "at", ", ".join(args.corner))
    print()
    for corner in args.corner:
        input_corner_type, corner_cells = corners[corner]
        if output_corner_type not in input_corner_type:
            print("Corner", corner, "doesn't support", output_corner_type, "(only {})".format(input_corner_type))
            return 1

        if output_corner_type == TimingType.basic and TimingType.ccsnoise in input_corner_type:
            input_corner_type = TimingType.ccsnoise
        else:
            input_corner_type = output_corner_type

        generate(
            libdir, lib,
            corner, output_corner_type, input_corner_type,
            corner_cells,
        )
    return 0


if __name__ == "__main__":
    import doctest
    fail, _ = doctest.testmod()
    if fail > 0:
        sys.exit(1)
    sys.exit(main())
