import json
import os
import logging
from projectsystem import VLQDecoder

logger = logging.getLogger("SWI")

def get_sourcemap_file(file_name):
    sourcemap_prefix = "//# sourceMappingURL="
    map_file = ""

    try:
        with open(file_name, "r", encoding="utf8") as f:
            # Read the last line of the file containing sourcemap information
            sourcemap_info = f.readlines()[-1]
            if sourcemap_info and len(sourcemap_info) > 0:
                sourcemap_info.strip()
                index = sourcemap_info.find(sourcemap_prefix)
                if index == 0:
                    map_file = sourcemap_info[len(sourcemap_prefix):].strip()
                    map_file = os.path.dirname(file_name) + os.path.sep + map_file
                    logger.info('    Found %s' % (sourcemap_info))
            f.close()
    except Exception as e:
        logger.info('    Could not read %s for sourceMappingURL: %s' % (file_name, str(e)))
        pass

    return map_file


class ParsedSourceMap:
    def __init__(self, file_name):
        self.authored_sources = None
        self.line_mappings = None
        
        logger.info('    Reading source map %s' % (file_name))

        try:
            self.content = None
            with open(file_name, "r", encoding="utf8") as f:
                self.content = json.loads(f.read())
                f.close()
    
            if self.content:
                self.root_path = os.path.abspath(os.path.dirname(file_name) + os.path.sep + self.content["sourceRoot"]) 
                self.version = self.content["version"]
                self.authored_sources = self.content["sources"]
                self.line_mappings = SourceMapParser.calculate_line_mappings(self.content)
        except Exception as e:
            logger.info('    Could not read source map %s: %s' % (file_name, str(e)))
            pass

    def is_valid(self):
        return not self.content is None and not self.authored_sources is None and not self.line_mappings is None

    def get_authored_sources_path(self):
        return [os.path.abspath(self.root_path + os.path.sep + x).lower() for x in self.authored_sources] if self.is_valid() else []


class LineMapping:
    # All line mappings are zero based
    def __init__(self):
        self.generated_line = 0
        self.generated_column = 0
        self.source_line = 0
        self.source_column = 0
        self.file_num = 0

    @staticmethod
    def compare_generated_mappings(mapping, line, column):
        return (column - mapping.generated_column) if (mapping.generated_line == line) else line - mapping.generated_line

    @staticmethod
    def compare_source_mappings(mapping, line, column):
        return (column - mapping.source_column) if (mapping.source_line == line) else line - mapping.source_line 

    @staticmethod
    def binary_search(line_mappings, line, column, comparator):
        max_index = len(line_mappings) - 1
        min_index = 0

        while (min_index <= max_index):
            mid = (max_index + min_index) >> 1

            comparison = comparator(line_mappings[mid], line, column)
            if (comparison > 0):
                min_index = mid + 1
            elif (comparison < 0):
                max_index = mid - 1
            else:
                max_index = mid
                break

        # Find the closest match
        result = max(min(len(line_mappings) - 1, max_index), 0)
        while (result + 1 < len(line_mappings) and comparator(line_mappings[result + 1], line, column) == 0):
            result += 1

        return result


class SourceMapParser:
    StartScopeSegmentDelimiter = '>'
    EndScopeSegmentDelimiter = '<'
    SegmentDelimiter = ','
    ScopeOrLineDelimiter = ';'

    @staticmethod
    def calculate_line_mappings(content):
        if (not content or
            content["version"] != 3 or
            not content["mappings"] or
            type(content["mappings"]) is not str or
            not content["sources"] or
            len(content["sources"]) is 0):
            return None

        max_file_num = len(content["sources"])
        last_mapping = LineMapping()
        generated_line = 0
        encoded_mappings = content["mappings"]
        current_file = 0
        parsing_index = 0
        length = len(encoded_mappings)

        mapping_list = []

        while parsing_index < length:
            if (encoded_mappings[parsing_index] == SourceMapParser.ScopeOrLineDelimiter):
                generated_line += 1
                parsing_index += 1
                last_mapping.generated_column = 0
            elif (encoded_mappings[parsing_index] == SourceMapParser.SegmentDelimiter):
                parsing_index += 1
            else:
                mapping = LineMapping()
                mapping.generated_line = generated_line

                # Get relative column offset
                result = VLQDecoder.decode(encoded_mappings, parsing_index)
                mapping.generated_column = last_mapping.generated_column + result["value"]
                last_mapping.generated_column = mapping.generated_column
                parsing_index += result["chars_read"]

                # Relative source index
                if (parsing_index < length and
                    encoded_mappings[parsing_index] != SourceMapParser.ScopeOrLineDelimiter and
                    encoded_mappings[parsing_index] != SourceMapParser.SegmentDelimiter):
                    result = VLQDecoder.decode(encoded_mappings, parsing_index)
                    current_file += result["value"]

                    if current_file > max_file_num:
                        return None

                    parsing_index += result["chars_read"]

                mapping.file_num = current_file

                # Relative source line
                if (parsing_index < length and
                    encoded_mappings[parsing_index] != SourceMapParser.ScopeOrLineDelimiter and
                    encoded_mappings[parsing_index] != SourceMapParser.SegmentDelimiter):
                    result = VLQDecoder.decode(encoded_mappings, parsing_index)
                    mapping.source_line = last_mapping.source_line + result["value"]
                    last_mapping.source_line = mapping.source_line
                    parsing_index += result["chars_read"]

                # Relative source column
                if (parsing_index < length and
                    encoded_mappings[parsing_index] != SourceMapParser.ScopeOrLineDelimiter and
                    encoded_mappings[parsing_index] != SourceMapParser.SegmentDelimiter):
                    result = VLQDecoder.decode(encoded_mappings, parsing_index)
                    mapping.source_column = last_mapping.source_column + result["value"]
                    last_mapping.source_column = mapping.source_column
                    parsing_index += result["chars_read"]

                # Check if there is a name, ignore it
                if (parsing_index < length and
                    encoded_mappings[parsing_index] != SourceMapParser.ScopeOrLineDelimiter and
                    encoded_mappings[parsing_index] != SourceMapParser.SegmentDelimiter):
                    result = VLQDecoder.decode(encoded_mappings, parsing_index)
                    parsing_index += result["chars_read"]

                mapping_list.append(mapping)

        return mapping_list
