#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (c) 2012, Cloudscaling
# All Rights Reserved.
#
#    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.

"""cloudinit HACKING file compliance testing (based off of nova hacking.py)

built on top of pep8.py
"""

import inspect
import logging
import re
import sys

import pep8

# Don't need this for testing
logging.disable('LOG')

# N1xx comments
# N2xx except
# N3xx imports
# N4xx docstrings
# N[5-9]XX (future use)

DOCSTRING_TRIPLE = ['"""', "'''"]
VERBOSE_MISSING_IMPORT = False
_missingImport = set([])


def import_normalize(line):
    # convert "from x import y" to "import x.y"
    # handle "from x import y as z" to "import x.y as z"
    split_line = line.split()
    if (line.startswith("from ") and "," not in line and
       split_line[2] == "import" and split_line[3] != "*" and
       split_line[1] != "__future__" and
       (len(split_line) == 4 or (len(split_line) == 6 and
                                 split_line[4] == "as"))):
        return "import %s.%s" % (split_line[1], split_line[3])
    else:
        return line


def cloud_import_alphabetical(physical_line, line_number, lines):
    """Check for imports in alphabetical order.

    HACKING guide recommendation for imports:
    imports in human alphabetical order
    N306
    """
    # handle import x
    # use .lower since capitalization shouldn't dictate order
    split_line = import_normalize(physical_line.strip()).lower().split()
    split_previous = import_normalize(lines[line_number - 2])
    split_previous = split_previous.strip().lower().split()
    # with or without "as y"
    length = [2, 4]
    if (len(split_line) in length and len(split_previous) in length and
            split_line[0] == "import" and split_previous[0] == "import"):
        if split_line[1] < split_previous[1]:
            return (0, "N306: imports not in alphabetical order (%s, %s)"
                    % (split_previous[1], split_line[1]))


def cloud_docstring_start_space(physical_line):
    """Check for docstring not start with space.

    HACKING guide recommendation for docstring:
    Docstring should not start with space
    N401
    """
    pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE])  # start
    if (pos != -1 and len(physical_line) > pos + 1):
        if (physical_line[pos + 3] == ' '):
            return (pos,
                    "N401: one line docstring should not start with a space")


def cloud_todo_format(physical_line):
    """Check for 'TODO()'.

    HACKING guide recommendation for TODO:
    Include your name with TODOs as in "#TODO(termie)"
    N101
    """
    pos = physical_line.find('TODO')
    pos1 = physical_line.find('TODO(')
    pos2 = physical_line.find('#')  # make sure it's a comment
    if (pos != pos1 and pos2 >= 0 and pos2 < pos):
        return pos, "N101: Use TODO(NAME)"


def cloud_docstring_one_line(physical_line):
    """Check one line docstring end.

    HACKING guide recommendation for one line docstring:
    A one line docstring looks like this and ends in a period.
    N402
    """
    pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE])  # start
    end = max([physical_line[-4:-1] == i for i in DOCSTRING_TRIPLE])  # end
    if (pos != -1 and end and len(physical_line) > pos + 4):
        if (physical_line[-5] != '.'):
            return pos, "N402: one line docstring needs a period"


def cloud_docstring_multiline_end(physical_line):
    """Check multi line docstring end.

    HACKING guide recommendation for docstring:
    Docstring should end on a new line
    N403
    """
    pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE])  # start
    if (pos != -1 and len(physical_line) == pos):
        print(physical_line)
        if (physical_line[pos + 3] == ' '):
            return (pos, "N403: multi line docstring end on new line")


current_file = ""


def readlines(filename):
    """Record the current file being tested."""
    pep8.current_file = filename
    return open(filename).readlines()


def add_cloud():
    """Monkey patch pep8 for cloud-init guidelines.

    Look for functions that start with cloud_
    and add them to pep8 module.

    Assumes you know how to write pep8.py checks
    """
    for name, function in globals().items():
        if not inspect.isfunction(function):
            continue
        if name.startswith("cloud_"):
            exec("pep8.%s = %s" % (name, name))

if __name__ == "__main__":
    # NOVA based 'hacking.py' error codes start with an N
    pep8.ERRORCODE_REGEX = re.compile(r'[EWN]\d{3}')
    add_cloud()
    pep8.current_file = current_file
    pep8.readlines = readlines
    try:
        pep8._main()
    finally:
        if len(_missingImport) > 0:
            print >> sys.stderr, ("%i imports missing in this test environment"
                                  % len(_missingImport))
