#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2017-2022 F4PGA 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
#
#     http://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

from sys import byteorder
from fasm.model import \
    SetFasmFeature, Annotation, FasmLine, ValueFormat
from fasm.parser import tags

TAG_TO_VALUE_FORMAT = {
    tags.plain: ValueFormat.PLAIN,
    tags.decimal: ValueFormat.VERILOG_DECIMAL,
    tags.hex: ValueFormat.VERILOG_HEX,
    tags.binary: ValueFormat.VERILOG_BINARY,
    tags.octal: ValueFormat.VERILOG_OCTAL
}
""" Maps tags from the parse_fasm library to ValueFormats """

# The following functions decode the binary format generated by parse_fasm.
# This is a lightweight binary format designed to be fast to decode.

cdef (int, int) get_header(char* tags, char* data, int i):
    """
    Match a tag and retrieve length from the header.
    Returns data length and offset, or None and the same offset
    if the header doesn't match on of the given tags.
    """
    if not data[i] in tags:
        return -1, i

    return int.from_bytes(data[i + 1:i + 5], byteorder), i + 5


def tagged_string_from_bytes(tag, data, i):
    """ Decode a tagged string. """
    length, i = get_header(tag, data, i)
    if length == -1:
        return None, i

    return data[i:i + length].decode('ascii'), i + length


def fasm_address_from_bytes(data, i):
    """ Decode a FASM address: [x:y] """
    length, i = get_header(tags.address, data, i)
    if length == -1:
        return None, None, i

    assert length == 4 or length == 8, length

    end = int.from_bytes(data[i:i + 4], byteorder)

    if length == 8:
        start = int.from_bytes(data[i + 4:i + 8], byteorder)
    else:  # If there is only one value, assign it to start
        start = end
        end = None

    return start, end, i + length


def fasm_value_from_bytes(data, i):
    """ Decode a FASM value. """
    tag = data[i:i+1]
    if tag == tags.plain:  # Matches a plain decimal integer.
        length = 4
        i = i + 1
    else:  # Matches Verilog number formats: hex, octal, binary, or decimal.
        length, i = get_header(
            tags.hex + tags.octal + tags.binary + tags.decimal, data, i)

    if length == -1:
        return None, None, i

    assert length % 4 == 0, length

    value = 0
    for j in range(i, i + length, 4):
        value = value << 32 | int.from_bytes(data[j:j + 4], byteorder)

    return value, TAG_TO_VALUE_FORMAT[tag], i + length


def fasm_width_from_bytes(data, i):
    """ Decode the width of a Verilog value. """
    if data[i:i+1] != tags.width:
        return None, i
    else:
        return int.from_bytes(data[i + 1:i + 5], byteorder), i + 5


def address_width(start, end):
    """ Calculate bit width inferred from the address. """
    return 1 if (start is None or end is None) else end - start + 1


def fasm_set_feature_from_bytes(data, i):
    """ Decode a set feature: feature = value """
    length, i = get_header(tags.set_feature, data, i)
    if length == -1:
        return None, i

    feature, p = tagged_string_from_bytes(tags.feature, data, i)
    start, end, p = fasm_address_from_bytes(data, p)
    width, p = fasm_width_from_bytes(data, p)
    value, value_format, p = fasm_value_from_bytes(data, p)

    assert p == i + length

    assert feature is not None

    if value is None:
        value = 1

    if width:
        assert value < 2 ** width, \
            "value {} larger than specified width of {}".format(value, width)

    assert value < 2**address_width(start, end), (value, start, end)

    return SetFasmFeature(
        feature=feature,
        start=start,
        end=end,
        value=value,
        value_format=value_format), i + length


def fasm_annotation_from_bytes(data, i):
    """ Decode an annotation: x = "y" """
    length, i = get_header(tags.annotation, data, i)
    if length == -1:
        return None, i

    name, p = tagged_string_from_bytes(tags.annotation_name, data, i)
    value, p = tagged_string_from_bytes(tags.annotation_value, data, p)

    assert p == i + length

    assert name is not None
    assert value is not None

    return Annotation(name=name, value=value), i + length


def fasm_annotations_from_bytes(data, i):
    """ Decode a set of annotations: { ... } """
    length, i = get_header(tags.annotations, data, i)
    if length == -1:
        return None, i

    annotations = []
    annotation, p = fasm_annotation_from_bytes(data, i)
    while annotation:
        annotations.append(annotation)
        annotation, p = fasm_annotation_from_bytes(data, p)

    assert p == i + length

    return annotations, i + length


def fasm_line_from_bytes(data, i):
    """ Decode an entire FASM line. """
    length, i = get_header(tags.line, data, i)
    if length == -1:
        return None, i

    set_feature, p = fasm_set_feature_from_bytes(data, i)
    annotations, p = fasm_annotations_from_bytes(data, p)
    comment, p = tagged_string_from_bytes(tags.comment, data, p)

    assert p == i + length

    assert (
        set_feature is not None or annotations is not None
        or comment is not None)

    return FasmLine(
        set_feature=set_feature, annotations=annotations,
        comment=comment), i + length


def parse_fasm_data(data):
    """ Parse FASM string, returning list of FasmLine named tuples."""
    lines = []
    line, p = fasm_line_from_bytes(data, 0)
    while line:
        lines.append(line)
        line, p = fasm_line_from_bytes(data, p)

    # Check that data read, plus the final null header,
    # is equal to the buffer size.
    assert p + 1 == len(data), p

    return lines
