# Copyright (C) 2017 The Meme Factory, Inc.  http://www.meme.com/
#
# 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.

# Karl O. Pinc <kop@meme.com>

'''
Produce report on the file system structure's failings.
'''

import os.path
try:
    from os import scandir
except ImportError:
    from scandir import scandir

from . import hidden


#
# Constants
#

# These could be configurable, but aren't.
LEVEL_INDENT = '  '               # How much to indent each dir level
MAX_FAILS_SHOWN = 5               # Limit on how many failed patterns are shown
POINTER = '^'                     # Used to mark a char in a string


#
# Functions
#

def render_headers(headers):
    indents = []
    indent = '{0}'
    for header in headers:
        indents.append(indent.format(header))
        indent = '{0}{1}'.format(LEVEL_INDENT, indent)
    return '\n'.join(indents)


def visual_indentation(headers):
    return [LEVEL_INDENT for cnt in range(len(headers))]


def print_error(printer, headers, name, msg):
    headers.append(name)
    printer.print_section(
        '\n'.join([render_headers(headers), msg]))
    headers.pop()


def match_errmsg(msg, patloc):
    return '\n'.join(['',
                      msg,
                      ('Matching row {0}: {1}'
                       .format(patloc.rownum, patloc.pattern))])


def plural(countable):
    if len(countable) > 1:
        return 's'
    else:
        return ''


def sort_patlocs(patlocs):
    sorted = list(patlocs)
    if len(sorted) <= 1:
        return sorted
    sorted.sort(key=lambda patloc: patloc.pos)
    sorted.sort(key=lambda patloc: patloc.rownum)
    return sorted


def least_pos_per_row(patlocs):
    '''Return the patlocs with the smallest pos per row, sorted by row.'''
    sorted = sort_patlocs(patlocs)
    if len(sorted) <= 1:
        return sorted[:]
    new = []
    lastrow = None
    for patloc in sorted:
        if patloc.rownum != lastrow:
            new.append(patloc)
            lastrow = patloc.rownum
    return new


def mismatched_rows_msg(patlocs):
    rowdigits = [str(pl.rownum) for pl in patlocs]
    return 'Mismatch of row{0}: {1}'.format(plural(rowdigits),
                                            ', '.join(rowdigits))


def matched_visual_pointer(vindent, pos):
    line = [vindent]
    line.extend([' ' for cnt in range(pos)])
    line.append(POINTER)
    return ''.join(line)


def render_failures(headers, patlocs):
    mismatches = len(patlocs)
    patlocs = patlocs[0:MAX_FAILS_SHOWN]
    vindent = ''.join(visual_indentation(headers))
    out = []
    lastpos = None
    for patloc in patlocs:
        if patloc.pos != lastpos:
            if lastpos is not None:
                out.append(matched_visual_pointer(vindent, lastpos))
            lastpos = patloc.pos
        line = [vindent, patloc.pattern]
        out.append(''.join(line))
    out.append(matched_visual_pointer(vindent, lastpos))
    if mismatches > MAX_FAILS_SHOWN:
        out.append(''.join([vindent, '...']))
    return '\n'.join(out)


def mismatch_errmsg(headers, vpointer, patlocs):
    uniques = least_pos_per_row(patlocs)
    return '\n'.join(
        [vpointer,
         mismatched_rows_msg(uniques),
         render_failures(headers, uniques)])


def report_dir_success(
        printer, analyzer, ignores, headers, direntry, patloc, report_items):
    rownum = patloc.rownum
    if rownum in analyzer.children:
        printer.add_report_items(direntry, report_items)
        dir_report(
            printer, analyzer.children[rownum], ignores, headers, direntry)
    else:
        print_error(printer, headers, direntry.name,
                    match_errmsg('This name must be a file, not a folder',
                                 patloc))


def report_file_success(
        printer, analyzer, headers, direntry, patloc, report_items):
    if patloc.rownum in analyzer.children:
        print_error(printer, headers, direntry.name,
                    match_errmsg('This name must be a folder, not a file',
                                 patloc))
    else:
        printer.add_report_items(direntry, report_items)


def report_success(
        printer, analyzer, ignores, headers, direntry, patlocs, report_items):
    patloc = patlocs.copy().pop()   # Ignore multiple matches
    if direntry.is_dir():
        report_dir_success(printer, analyzer, ignores, headers, direntry,
                           patloc, report_items)
    else:
        report_file_success(
            printer, analyzer, headers, direntry, patloc, report_items)


def unmatched_visual_pointer(headers, stripped_name, unmatched):
    if unmatched is True:
        remainders = 0
    else:
        remainders = len(unmatched)
    elements = visual_indentation(headers)
    elements.extend([' ' for cnt in range(len(stripped_name) - remainders)])
    elements.append(POINTER)
    return ''.join(elements)


def report_failure(printer, headers, direntry, stripped_name,
                   unmatched, patlocs):
    print_error(
        printer, headers, direntry.name,
        mismatch_errmsg(
            headers,
            unmatched_visual_pointer(headers, stripped_name, unmatched),
            patlocs))


def strip_extension(direntry):
    if direntry.is_dir():
        return direntry.name
    else:
        return os.path.splitext(direntry.name)[0]


def entry_report(printer, analyzer, ignores, headers, direntry):
    stripped_name = strip_extension(direntry)
    (unmatched, patlocs, report_items) = analyzer.invalid(stripped_name)
    if unmatched:
        report_failure(
            printer, headers, direntry, stripped_name, unmatched, patlocs)
    else:
        report_success(printer, analyzer, ignores, headers, direntry,
                       patlocs, report_items)


def get_name(direntry):
    return direntry.name.lower()


def dir_report(printer, analyzer, ignores, headers, direntry):
    headers.append(direntry.name)

    direntries = list(scandir(direntry.path))
    direntries.sort(key=get_name)
    for direntry in direntries:
        if direntry.name not in ignores and not hidden.is_hidden(direntry):
            entry_report(printer, analyzer, ignores, headers, direntry)

    headers.pop()


def analyze_dirs(printer, analyzer, config, path='.'):
    dirs = list(scandir(path))
    if not dirs:
        printer.print_section(
            'Empty directory, no content found')
        return
    dirs.sort(key=get_name)
    for dir in dirs:
        if dir.is_dir():
            dir_report(printer, analyzer, config['ignored_names'], [], dir)
        else:
            printer.print_section(
                'Not a directory: {}'.format(dir.name))
    printer.print_report_items()
