#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Advent of Code 2015 from http://adventofcode.com/2015/day/5
Author: James Walker
Copyrighted 2017 under the MIT license:
  http://www.opensource.org/licenses/mit-license.php
Execution:
  python advent_of_code_2015_day_05.py

--- Day 5: Doesn't He Have Intern-Elves For This? ---

  Santa needs help figuring out which strings in his text file are naughty or
  nice.
  
  A nice string is one with all of the following properties:
    It contains at least three vowels (aeiou only), like aei, xazegov, or 
      aeiouaeiouaeiou.
    It contains at least one letter that appears twice in a row, like xx,
      abcdde (dd), or aabbccdd (aa, bb, cc, or dd).
    It does not contain the strings ab, cd, pq, or xy, even if they are part of
      one of the other requirements.

  For example:
    ugknbfddgicrmopn is nice because it has at least three vowels
      (u...i...o...), a double letter (...dd...), and none of the disallowed
      substrings.
    aaa is nice because it has at least three vowels and a double letter, even
      though the letters used by different rules overlap.
    jchzalrnumimnmhp is naughty because it has no double letter.
    haegwjzuvuyypxyu is naughty because it contains the string xy.
    dvszwmarrgswjxmb is naughty because it contains only one vowel.

  How many strings are nice?
    Answer: 258

--- Day 5: Part Two ---

  Realizing the error of his ways, Santa has switched to a better model of
  determining whether a string is naughty or nice. None of the old rules apply,
  as they are all clearly ridiculous. Now, a nice string is one with all of the
  following properties:

    It contains a pair of any two letters that appears at least twice in the
      string without overlapping, like xyxy (xy) or aabcdefgaa (aa), but not
      like aaa (aa, but it overlaps).
    It contains at least one letter which repeats with exactly one letter
      between them, like xyx, abcdefeghi (efe), or even aaa.

  For example:
    qjhvhtzxzqqjkmpb is nice because is has a pair that appears twice (qj) and
      a letter that repeats with exactly one letter between them (zxz).
    xxyxx is nice because it has a pair that appears twice and a letter that
      repeats with one between, even though the letters used by each rule
      overlap.
    uurcxstgmygtbstg is naughty because it has a pair (tg) but no repeat with a
      single letter between them.
    ieodomkazucvgmuy is naughty because it has a repeating letter with one
      between (odo), but no pair that appears twice.

  How many strings are nice under these new rules?
    Answer: 53
"""


import collections
import os
import re
import sys


TestCase = collections.namedtuple('TestCase', 'input expected1 expected2')


class Advent_Of_Code_2015_Solver_Day05(object):
    """Advent of Code 2015 Day 5: Doesn't He Have Intern-Elves For This?"""

    def __init__(self, file_name=None):
        self._file_name = file_name
        self._puzzle_input = None
        self._solved_output = (
            "The text file had {0} nice strings using the original rules\n"
            "and it had {1} nice strings using the new rules."
        )
        self.__regex_vowels = re.compile('[aeiou]')
        self.__regex_double_char = re.compile('(\w)\\1+')
        self.__regex_naughty = re.compile('ab|cd|pq|xy')
        self.__regex_double_pair = re.compile('(\w{2})\w*\\1')
        self.__regex_triplet = re.compile('(\w)\w\\1')

    def _load_puzzle_file(self):
        filePath = "{dir}/{f}".format(dir=os.getcwd(), f=self._file_name)
        try:
            with open(filePath, mode='r') as puzzle_file:
                self._puzzle_input = puzzle_file.readlines()
        except IOError as err:
            errorMsg = (
                "ERROR: Failed to read the puzzle input from file '{file}'\n"
                "{error}"
            )
            print(errorMsg.format(file=self._file_name, error=err))
            exit(1)

    def __is_nice_string_using_old_rules(self, string):
        return (self.__regex_naughty.search(string) is None
            and len(self.__regex_vowels.findall(string)) > 2
            and self.__regex_double_char.search(string))

    def __is_nice_string_using_new_rules(self, string):
        return (self.__regex_double_pair.search(string)
            and self.__regex_triplet.search(string))

    def _solve_puzzle_parts(self):
        old_nice_count = 0
        new_nice_count = 0
        for string in self._puzzle_input:
            if not string:
                continue
            if self.__is_nice_string_using_old_rules(string):
                old_nice_count += 1
            if self.__is_nice_string_using_new_rules(string):
                new_nice_count += 1
        return (old_nice_count, new_nice_count)

    def get_puzzle_solution(self, alt_input=None):
        if alt_input is None:
            self._load_puzzle_file()
        else:
            self._puzzle_input = alt_input
        old_nice_count, new_nice_count = self._solve_puzzle_parts()
        return self._solved_output.format(old_nice_count, new_nice_count)

    def _run_test_case(self, test_case):
        correct_output = self._solved_output.format(
            test_case.expected1,
            test_case.expected2
        )
        test_output = self.get_puzzle_solution(test_case.input)
        if correct_output == test_output:
            print("Test passed for input '{0}'".format(test_case.input))
        else:
            print("Test failed for input '{0}'".format(test_case.input))
            print(test_output)

    def run_test_cases(self):
        print("No Puzzle Input for {puzzle}".format(puzzle=self.__doc__))
        print("Running Test Cases...")
        self._run_test_case(TestCase(['ugknbfddgicrmopn'], 1, 0))
        self._run_test_case(TestCase(['aaa'], 1, 0))
        self._run_test_case(TestCase(['jchzalrnumimnmhp'], 0, 0))
        self._run_test_case(TestCase(['haegwjzuvuyypxyu'], 0, 0))
        self._run_test_case(TestCase(['dvszwmarrgswjxmb'], 0, 0))
        self._run_test_case(TestCase(['xyxy'], 0, 1))
        self._run_test_case(TestCase(['aabcdefgaa'], 0, 0))
        self._run_test_case(TestCase(['qjhvhtzxzqqjkmpb'], 0, 1))
        self._run_test_case(TestCase(['xxyxx'], 0, 1))
        self._run_test_case(TestCase(['uurcxstgmygtbstg'], 0, 0))
        self._run_test_case(TestCase(['ieodomkazucvgmuy'], 0, 0))
        self._run_test_case(TestCase(['aaccacc'], 1, 1))


if __name__ == '__main__':
    try:
        day05_solver = Advent_Of_Code_2015_Solver_Day05(sys.argv[1])
        print(day05_solver.__doc__)
        print(day05_solver.get_puzzle_solution())
    except IndexError:
        Advent_Of_Code_2015_Solver_Day05().run_test_cases()
