#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  king_phisher/utilities.py
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are
#  met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above
#    copyright notice, this list of conditions and the following disclaimer
#    in the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of the project nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
#  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
#  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
#  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

import argparse
import collections
import datetime
import functools
import gc
import inspect
import json
import logging
import operator
import os
import posixpath as webpath
import random
import re
import string
import sys
import threading

from king_phisher import color
from king_phisher import constants
from king_phisher import find
from king_phisher import its
from king_phisher import startup

import dateutil
import dateutil.tz
import email_validator
import jsonschema
import smoke_zephyr.utilities

EMAIL_REGEX = re.compile(r'^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,9}$', flags=re.IGNORECASE)
TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S'

class FreezableDict(collections.OrderedDict):
	"""
	A dictionary that can be frozen to prevent further editing. Useful for
	debugging. If any function tries to edit a frozen dictionary, a
	:py:exc:`RuntimeError` will be raised and a traceback will occur.
	"""
	__slots__ = ('_frozen',)
	def __init__(self, *args, **kwargs):
		self._frozen = False
		super(FreezableDict, self).__init__(*args, **kwargs)

	def __repr__(self):
		return "<{0} frozen={1} {2}>".format(self.__class__.__name__, self._frozen, super(FreezableDict, self).__repr__())

	def __setitem__(self, *args, **kwargs):
		if self._frozen:
			raise RuntimeError('Frozen dictionary cannot be modified')
		super(FreezableDict, self).__setitem__(*args, **kwargs)

	def __delitem__(self, *args, **kwargs):
		if self._frozen:
			raise RuntimeError('Frozen dictionary cannot be modified')
		super(FreezableDict, self).__delitem__(*args, **kwargs)

	def pop(self, *args, **kwargs):
		if self._frozen:
			raise RuntimeError('Frozen dictionary cannot be modified')
		super(FreezableDict, self).pop(*args, **kwargs)

	def update(self, *args, **kwargs):
		if self._frozen:
			raise RuntimeError('Frozen dictionary cannot be modified')
		super(FreezableDict, self).update(*args, **kwargs)

	def popitem(self, *args, **kwargs):
		if self._frozen:
			raise RuntimeError('Frozen dictionary cannot be modified')
		super(FreezableDict, self).popitem(*args, **kwargs)

	def clear(self, *args, **kwargs):
		if self._frozen:
			raise RuntimeError('Frozen dictionary cannot be modified')
		super(FreezableDict, self).clear(*args, **kwargs)

	def freeze(self):
		"""
		Freeze the dictionary to prevent further editing.
		"""
		self._frozen = True

	def thaw(self):
		"""
		Thaw the dictionary to once again enable editing.
		"""
		self._frozen = False

	@property
	def frozen(self):
		"""
		Whether or not the dictionary is frozen and can not be modified.

		:rtype: bool
		"""
		return self._frozen

class Mock(object):
	"""
	A fake object used to replace missing imports when generating documentation.
	"""
	__all__ = []
	def __init__(self, *args, **kwargs):
		pass

	def __add__(self, other):
		return other

	def __call__(self, *args, **kwargs):
		return Mock()

	@classmethod
	def __getattr__(cls, name):
		if name in ('__file__', '__path__'):
			return os.devnull
		else:
			return Mock()

	def __or__(self, other):
		return other

	@classmethod
	def __setattr__(cls, name, value):
		pass

	def __getitem__(self, name):
		return Mock()

	def __setitem__(self, name, value):
		pass

def argp_add_args(parser, default_root=''):
	"""
	Add standard arguments to a new :py:class:`argparse.ArgumentParser` instance
	for configuring logging options from the command line and displaying the
	version information.

	.. note::
		This function installs a hook to *parser.parse_args* to automatically
		handle options which it adds. This includes setting up a stream logger
		based on the added options.

	:param parser: The parser to add arguments to.
	:type parser: :py:class:`argparse.ArgumentParser`
	:param str default_root: The default root logger to specify.
	"""
	startup.argp_add_default_args(parser, default_root=default_root)

	@functools.wraps(parser.parse_args)
	def parse_args_hook(*args, **kwargs):
		arguments = parser._parse_args(*args, **kwargs)
		configure_stream_logger(arguments.logger, arguments.loglvl)
		gc.set_debug(arguments.gc_debug_stats | arguments.gc_debug_leak)
		return arguments
	parser._parse_args = parser.parse_args
	parser.parse_args = parse_args_hook
	return parser

def assert_arg_type(arg, arg_type, arg_pos=1, func_name=None):
	"""
	Check that an argument is an instance of the specified type and if not
	raise a :py:exc:`TypeError` exception with a meaningful message. If
	*func_name* is not specified, it will be determined by examining the stack.

	:param arg: The argument to check.
	:param arg_type: The type or sequence of types that *arg* can be.
	:type arg_type: list, tuple, type
	:param int arg_pos: The position of the argument in the function.
	:param str func_name: The name of the function the argument is for.
	"""
	if isinstance(arg, arg_type):
		return
	if func_name is None:
		parent_frame = inspect.stack()
[1][0]
		func_name = parent_frame.f_code.co_name
	if isinstance(arg_type, (list, tuple)):
		if len(arg_type) == 1:
			arg_type = arg_type[0].__name__
		else:
			arg_type = tuple(at.__name__ for at in arg_type)
			ar
g_type = ', '.join(arg_type[:-1]) + ' or ' + arg_type[-1]
	else:
		arg_type = arg_type.__name__
	raise TypeError("{0}() argument {1} must be {2}, not {3}".format(func_name, arg_pos, arg_type, type(arg).__name__))

def configure_stream_logger(logger, level=None):
	"""
	Configure the default stream handler for logging messages to the console.
	This also configures the basic logging environment for the application.

	:param str logger: The logger to add the stream handler for.
	:param level: The level to set the logger to, will default to WARNING if no level is specified.
	:type level: None, int, str
	:return: The new configured stream handler.
	:rtype: :py:class:`logging.StreamHandler`
	"""
	if level is None:
		level = constants.DEFAULT_LOG_LEVEL
	if isinstance(level, str):
		level = getattr(logging, level)
	root_logger = logging.getLogger('')
	for handler in root_logger.handlers:
		root_logger.removeHandler(handler)

	logging.getLogger(logger).setLevel(logging.DEBUG)
	console_log_handler = logging.StreamHandler()
	console_log_handler.setLevel(level)
	if its.on_linux:
		console_log_handler.setFormatter(color.ColoredLogFormatter('%(levelname)s %(message)s'))
	else:
		console_log_handler.setFormatter(logging.Formatter('%(levelname)-8s %(message)s'))
	logging.getLogger(logger).addHandler(console_log_handler)
	logging.captureWarnings(True)
	return console_log_handler

def datetime_local_to_utc(dt):
	"""
	Convert a :py:class:`datetime.datetime` instance from the local time to UTC
	time.

	:param dt: The time to convert from local 