"""Contains all the core ShutIt methods and functionality, and public interface
off to internal objects such as shutit_pexpect.
"""

# The MIT License (MIT)
#
# Copyright (C) 2014 OpenBet Limited
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# ITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import sys
import os
import socket
import time
import getpass
import datetime
import logging
import fcntl
import pwd
import re
import termios
import signal
import struct
import threading
from distutils.dir_util import mkpath
import curtsies
#from curtsies.fmtfuncs import black, yellow, magenta, cyan, gray, blue, red, green, on_black, on_dark, on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_gray, bold, dark, underline, blink, invert, plain
from curtsies.fmtfuncs import blue, cyan, invert
from shutit_session_setup import vagrant
import shutit_threads

if sys.version_info[0] >= 3:
	unicode = str

class ShutItGlobal(object):
	"""Single object to store all the separate ShutIt sessions, and information
	that is global to all sessions.
	"""

	only_one = None
	def __init__(self):
		"""Constructor.
		"""
		self.shutit_objects = []
		# Primitive singleton enforcer.
		assert self.only_one is None, shutit_util.print_debug()
		self.only_one                = True

		# Capture the original working directory.
		self.owd                     = os.getcwd()

		# Python version.
		self.ispy3                   = (sys.version_info[0] >= 3)

		# Threading.
		self.global_thread_lock      = threading.Lock()
		# Acquire the lock by default.
		self.global_thread_lock.acquire()

		# Secret words.
		self.secret_words_set        = set()

		# Logging.
		# global logstream is written to by logger in each shutit_class.py object.
		self.logstream               = None
		self.logstream_size          = 1000000
		self.log_trace_when_idle     = False
		self.signal_id               = None
		self.window_size_max         = 65535
		self.username                = os.environ.get('LOGNAME', '')
		self.default_timeout         = 3600
		self.delaybeforesend         = 0
		self.default_encoding        = 'utf-8'

		# Panes.
		self.managed_panes           = False
		self.pane_manager            = None
		self.lower_pane_rotate_count = 0

		# Errors.
		self.stacktrace_lines_arr    = []

		# Prompts and shell.
		self.bash_startup_command    = "bash --noprofile --rcfile <(sleep .05||sleep 1)"
		# Quotes here are intentional. Some versions of sleep don't support fractional seconds.
		# True is called to take up the time require
		self.prompt_command          = "'sleep .05||sleep 1'"
		# It's important that this has '.*' at the start, so the matched data is reliably 'after' in the
		# child object. Use these where possible to make things more consistent.
		# Attempt to capture any starting prompt (when starting) with this regexp.
		# The '>' is for AIX and explains why we use '2>/dev/null' in some other parts
		# of the code (ie to avoid matching initialiser commands).
		self.base_prompt      = '\n.*[@#$] '
		# There is a problem with lines roughly around this length + the length of the prompt (?3k?)
		self.line_limit          = 3000
		# Terminal size
		def terminal_size():
			h, w, _, _ = struct.unpack('HHHH', fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))
			return int(h), int(w)
		try:
			self.root_window_size = terminal_size()
		except IOError:
			# If no terminal exists, set to default.
			self.root_window_size = (24,80)
		# Just override to the max possible
		self.pexpect_window_size = (self.window_size_max,self.window_size_max)
		self.interactive         = 1 # Default to true until we know otherwise

		# Session environments.
		# Environments are kept globally, as different sessions may re-connect to them.
		self.shutit_pexpect_session_environments = set()

		# Real username.
		if self.username == '':
			try:
				if os.getlogin() != '':
					self.username = os.getlogin()
			except Exception:
				self.username = getpass.getuser()
			if self.username == '':
				self.handle_exit(msg='LOGNAME not set in the environment, ' + 'and login unavailable in python; ' + 'please set to your username.', exit_code=1)
		self.real_user        = os.environ.get('SUDO_USER', self.username)
		self.real_user_id     = pwd.getpwnam(self.real_user).pw_uid

		# ShutIt build ID.
		self.build_id         = (socket.gethostname() + '_' + self.real_user + '_' + str(time.time()) + '.' + str(datetime.datetime.now().microsecond))
		# ShutIt state directory.
		shutit_state_dir_base  = '/tmp/shutit_' + self.username
		if not os.access(shutit_state_dir_base,os.F_OK):
			mkpath(shutit_state_dir_base,mode=0o777)
		self.shutit_state_dir       = shutit_state_dir_base + '/' + self.build_id
		os.chmod(shutit_state_dir_base,0o777)
		if not os.access(self.shutit_state_dir,os.F_OK):
			mkpath(self.shutit_state_dir,mode=0o777)
		os.chmod(self.shutit_state_dir,0o777)
		self.shutit_state_dir_build_db_dir = self.shutit_state_dir + '/build_db'

		# Allowed delivery methods.
		self.allowed_delivery_methods = ['ssh','dockerfile','bash','docker','vagrant']

	def __str__(self):
		str_repr = '\n====== SHUTIT_GLOBAL_OBJECT BEGIN ====='
		str_repr += '\tself.signal_id='          + str(self.signal_id)
		str_repr += '\tself.window_size_max='    + str(self.window_size_max)
		str_repr += '\tself.username='           + str(self.username)
		str_repr += '\tbase_prompt='             + str(self.base_prompt)
		str_repr += '\treal_user='               + str(self.real_user)
		str_repr += '\treal_user_id='            + str(self.real_user_id)
		str_repr += '\tbuild_id='                + str(self.build_id)
		str_repr += '\tdelaybeforesend='         + str(self.delaybeforesend)
		str_repr += '\tprompt_command='          + str(self.prompt_command)
		str_repr += '\tself.default_encoding='   + str(self.default_encoding)
		for shutit_object in self.shutit_objects:
			str_repr += str(shutit_object)
		str_repr += '\n====== SHUTIT_GLOBAL_OBJECT DONE ====='
		return str_repr



	def create_session(self,
	                   session_type='bash',
	                   docker_image=None,
	                   rm=None,
	                   echo=False,
	                   walkthrough=False,
	                   walkthrough_wait=-1,
	                   nocolor=False,
	                   loglevel='WARNING'):
		assert isinstance(session_type, str), shutit_util.print_debug()
		new_shutit = ShutIt(standalone=True, session_type=session_type)
		self.shutit_objects.append(new_shutit)
		if session_type == 'bash':
			new_shutit.process_args(ShutItInit('build',
			                                   delivery='bash',
			                                   echo=echo,
			                                   walkthrough=walkthrough,
			                                   walkthrough_wait=walkthrough_wait,
			                                   loglevel=loglevel))
			new_shutit.load_configs()
			new_shutit.setup_host_child_environment()
			return new_shutit
		elif session_type == 'docker':
			new_shutit.process_args(ShutItInit('build',
			                                   delivery='docker',
			                                   base_image=docker_image,
			                                   echo=echo,
			                                   walkthrough=walkthrough,
			                                   walkthrough_wait=walkthrough_wait,
			                                   loglevel=loglevel))
			new_shutit.target['rm'] = rm
			target_child = new_shutit.conn_docker_start_container('target_child')
			new_shutit.setup_host_child_environment()
			new_shutit.setup_target_child_environment(target_child)
			return new_shutit
		new_shutit.fail('unhandled session type: ' + session_type)
		return new_shutit


	def create_session_vagrant(self,
	                           session_name,
	                           num_machines,
	                           vagrant_image,
	                           vagrant_provider,
	                           gui,
	                           memory,
	                           swapsize,
	                           echo,
	                           walkthrough,
	                           walkthrough_wait,
	                           nocolor,
	                           vagrant_version,
	                           virt_method,
	                           root_folder,
	                           cpu,
	                           loglevel):
		new_shutit = ShutIt(standalone=True, session_type='vagrant')
		self.shutit_objects.append(new_shutit)
		# Vagrant is: delivery over bash, but running the vagrant scripts first.
		new_shutit.process_args(ShutItInit('build',
		                                   delivery='bash',
		                                   echo=echo,
		                                   walkthrough=walkthrough,
		                                   walkthrough_wait=walkthrough_wait,
		                                   nocolor=nocolor,
		                                   loglevel=loglevel))
		new_shutit.load_configs()
		new_shutit.setup_host_child_environment()
		# Run vagrant setup now
		vagrant.pre_build(shutit=new_shutit,
		                  vagrant_version=vagrant_version,
		                  virt_method=virt_method)
		machines = vagrant.setup_machines(new_shutit,
		                                  vagrant_image,
		                                  virt_method,
		                                  gui,
		                                  memory,
		                                  root_folder,
		                                  session_name,
		                                  swapsize,
		                                  num_machines,
		                                  cpu)
		new_shutit.vagrant_machines = machines
		return new_shutit


	def determine_interactive(self):
		"""Determine whether we're in an interactive shell.
		Sets interactivity off if appropriate.
		cf http://stackoverflow.com/questions/24861351/how-to-detect-if-python-script-is-being-run-as-a-background-process
		"""
		try:
			if not sys.stdout.isatty() or os.getpgrp() != os.tcgetpgrp(sys.stdout.fileno()):
				self.interactive = 0
				return False
		except Exception:
			self.interactive = 0
			return False
		if self.interactive == 0:
			return False
		return True


	def setup_panes(self, action=None):
		assert not self.managed_panes or (self.managed_panes and self.logstream)
		assert action is not None
		# TODO: managed_panes and echo are incompatible
		if self.managed_panes:
			self.pane_manager     = PaneManager(self)
			shutit_threads.track_main_thread()
		else:
			if action == 'build':
				shutit_threads.track_main_thread_simple()


	def yield_to_draw(self):
		# Release the lock to allow the screen to be drawn, then acquire again.
		# Only ever yield if there are any sessions to draw.
		if len(get_shutit_pexpect_sessions()) > 0:
			self.global_thread_lock.release()
			# Allow a _little_ time for others to get a look in
			time.sleep(0.001)
			self.global_thread_lock.acquire()


	def handle_exit(self,
	                exit_code=0,
	                msg=None):
		if not msg:
			msg = '\r\nExiting with error code: ' + str(exit_code)
			msg += '\r\nInvoking command was: ' + sys.executable
			for arg in sys.argv:
				msg += ' ' + arg
		if exit_code != 0:
			self.shutit_print('\r\nExiting with error code: ' + str(exit_code))
			self.shutit_print(msg)
			self.shutit_print('\r\nResetting terminal')
			shutit_util.sanitize_terminal()
			shutit_util.exit_cleanup()
		sys.exit(exit_code)


	def shutit_print(self, msg):
		"""Handles simple printing of a msg at the global level.
		"""
		if self.pane_manager is None:
			print(msg)


# shutit_global.shutit_objects have the pexpect sessions in their shutit_pexpect_sessions variable.
class PaneManager(object):
	only_one = None
	def __init__(self, shutit_global_object):
		"""

		only_one             - singleton insurance
		"""
		assert self.only_one is None
		self.only_one is True
		# Keep it simple for now by creating four panes
		self.shutit_global             = shutit_global_object
		self.top_left_session_pane     = SessionPane('top_left')
		self.top_right_session_pane    = SessionPane('top_right')
		self.bottom_left_session_pane  = SessionPane('bottom_left')
		self.bottom_right_session_pane = SessionPane('bottom_right')
		self.window                    = None
		self.screen_arr                = None
		self.wheight                   = None
		self.wwidth                    = None
		# Whether to actually draw the screen - defaults to 'True'
		self.do_render                 = True
		# Refresh the window
		self.refresh_window()


	def refresh_window(self):
		self.window               = curtsies.FullscreenWindow(hide_cursor=True)
		self.wheight              = self.window.height
		self.wwidth               = self.window.width
		self.screen_arr           = None
		# Divide the screen up into two, to keep it simple for now
		self.wheight_top_end      = int(self.wheight / 2)
		self.wheight_bottom_start = int(self.wheight / 2)
		self.wwidth_left_end      = int(self.wwidth / 2)
		self.wwidth_right_start   = int(self.wwidth / 2)
		assert self.wheight >= 24, 'Terminal not tall enough: ' + str(self.wheight) + ' < 24'
		assert self.wwidth >= 80, 'Terminal not wide enough: ' + str(self.wwidth) + ' < 80'


	def draw_screen(self, draw_type='default', quick_help=None):
		if quick_help is None:
			quick_help = 'Help: (r)otate shutit sessions | re(d)raw screen | (1,2,3,4) zoom pane in/out | (q)uit'
		assert draw_type in ('default','clearscreen','zoomed1','zoomed2','zoomed3','zoomed4')
		# Header
		header_text = u'  <= Shutit'
		self.screen_arr           = curtsies.FSArray(self.wheight, self.wwidth)
		self.screen_arr[0:1,0:len(header_text)] = [blue(header_text)]
		# Footer
		space = (self.wwidth - len(quick_help))*' '
		footer_text = space + quick_help
		if not self.shutit_global.ispy3:
			footer_text = footer_text.decode('utf-8')
		self.screen_arr[self.wheight-1:self.wheight,0:len(footer_text)] = [invert(blue(footer_text))]
		if draw_type in ('default','zoomed3','zoomed4'):
			# get sessions - for each ShutIt object in shutit_global
			sessions = list(get_shutit_pexpect_sessions())
			# reverse sessions as we're more likely to be interested in later ones.
			sessions.reverse()
			# Update the lower_pane_rotate_count so that it doesn't exceed the length of sessions.
			self.shutit_global.lower_pane_rotate_count = self.shutit_global.lower_pane_rotate_count % len(sessions)
			sessions = sessions[-self.shutit_global.lower_pane_rotate_count:] + sessions[:-self.shutit_global.lower_pane_rotate_count]
		# Truncate logstream if it gets too big.
		if self.shutit_global.logstream.getvalue() > self.shutit_global.logstream_size:
			self.shutit_global.logstream.truncate(self.shutit_global.logstream_size)
		if draw_type == 'default':
			# Draw the sessions.
			self.do_layout_default()
			logstream_lines = []
			logstream_string_lines_list = self.shutit_global.logstream.getvalue().split('\n')
			for line in logstream_string_lines_list:
				logstream_lines.append(SessionPaneLine(line,time.time(),'log'))
			self.write_out_lines_to_fit_pane(self.top_left_session_pane, logstream_lines, u'Logs')
			self.write_out_lines_to_fit_pane(self.top_right_session_pane, self.shutit_global.stacktrace_lines_arr, u'Code Context')
			# Count two sessions
			count = 0
			for shutit_pexpect_session in sessions:
				count += 1
				if count == 2:
					self.write_out_lines_to_fit_pane(self.bottom_left_session_pane,
					                                 shutit_pexpect_session.session_output_lines,
					                                 u'Shutit Session: ' + str(shutit_pexpect_session.pexpect_session_number) + '/' + str(len(sessions)))
				elif count == 1:
					self.write_out_lines_to_fit_pane(self.bottom_right_session_pane,
					                                 shutit_pexpect_session.session_output_lines,
					                                 u'ShutIt Session: ' + str(shutit_pexpect_session.pexpect_session_number) + '/' + str(len(sessions)))
				else:
					break
		elif draw_type == 'zoomed1':
			self.do_layout_zoomed(zoom_number=1)
			logstream_lines = []
			logstream_string_lines_list = self.shutit_global.logstream.getvalue().split('\n')
			for line in logstream_string_lines_list:
				logstream_lines.append(SessionPaneLine(line,time.time(),'log'))
			self.write_out_lines_to_fit_pane(self.top_left_session_pane, logstream_lines, u'Logs')
		elif draw_type == 'zoomed2':
			self.do_layout_zoomed(zoom_number=2)
			self.write_out_lines_to_fit_pane(self.top_left_session_pane, self.shutit_global.stacktrace_lines_arr, u'Code Context')
		elif draw_type == 'zoomed3':
			self.do_layout_zoomed(zoom_number=3)
			# Get first session
			count = 0
			for shutit_pexpect_session in sessions:
				count += 1
				if count == 2:
					self.write_out_lines_to_fit_pane(self.top_left_session_pane,
					                                 shutit_pexpect_session.session_output_lines,
					                                 u'Shutit Session: ' + str(shutit_pexpect_session.pexpect_session_number) + '/' + str(len(sessions)))
				elif count > 2:
					break
		elif draw_type == 'zoomed4':
			self.do_layout_zoomed(zoom_number=4)
			# Get second session
			for shutit_pexpect_session in sessions:
				self.write_out_lines_to_fit_pane(self.top_left_session_pane,
					                                 shutit_pexpect_session.session_output_lines,
					                                 u'ShutIt Session: ' + str(shutit_pexpect_session.pexpect_session_number) + '/' + str(len(sessions)))
				break
		elif draw_type == 'clearscreen':
			for y in range(0,self.wheight):
				line = u' '*self.wwidth
				self.screen_arr[y:y+1,0:len(line)] = [line]
		else:
			assert False, 'Layout not handled: ' + draw_type
		if self.do_render:
			self.window.render_to_terminal(self.screen_arr, cursor_pos=(0,0))


	def write_out_lines_to_fit_pane(self, pane, p_lines, title):
		assert pane is not None
		assert isinstance(pane, SessionPane)
		assert isinstance(title, unicode)
		pane_width  = pane.get_width()
		pane_height = pane.get_height()
		assert pane_width > 39
		assert pane_height > 19
		# We reserve one row at the end as a pane status line
		available_pane_height   = pane.get_height() - 1
		lines_in_pane_str_arr   = []
		p_lines_str = []
		for session_pane_line in p_lines:
			assert isinstance(session_pane_line, SessionPaneLine)
			p_lines_str.append(session_pane_line.line_str)
		p_lines = p_lines_str
		p_lines_str = None
		# Scrub any ansi escape sequences.
		ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
		if not self.shutit_global.ispy3:
			lines = [ ansi_escape.sub('', line).strip().decode('utf-8') for line in p_lines ]
		else:
			lines = [ ansi_escape.sub('', line).strip() for line in p_lines ]
		# If the last line is blank we can just skip it.
		if len(lines) > 0 and lines[-1] == '':
			lines = lines[:-1]
		for line in lines:
			# Take the next line in the stream. If it's greater than the pane_width,
			# Then parcel over multiple lines
			while len(line) > pane_width-1 and len(line) > 0:
				lines_in_pane_str_arr.append(line[:pane_width-1])
				line = line[pane_width-1:]
			lines_in_pane_str_arr.append(line)
		# Status line:
		lines_in_pane_str_arr.append(title)
		top_y                                      = pane.top_left_y
		bottom_y                                   = pane.bottom_right_y
		for i, line in zip(reversed(range(top_y,bottom_y)), reversed(lines_in_pane_str_arr)):
			# Status on bottom line
			# If    this is on the top, and height + top_y value == i (ie this is the last line of the pane)
			#    OR this is on the bottom (ie top_y is not 1), and height + top_y == i
			# One or both of these help prevent glitches on the screen. Don't know why. Maybe replace with more standard list TODO
			if (top_y == 1 and available_pane_height + top_y == i) or (top_y != 1 and available_pane_height + top_y == i):
				self.screen_arr[i:i+1, pane.top_left_x:pane.top_left_x+len(line)] = [cyan(invert(line))]
			else:
				self.screen_arr[i:i+1, pane.top_left_x:pane.top_left_x+len(line)] = [line]



	def do_layout_zoomed(self, zoom_number):
		# Only one window - the top left.
		self.top_left_session_pane.set_position    (top_left_x=0,
		                                            top_left_y=1,
		                                            bottom_right_x=self.wwidth,
		                                            bottom_right_y=self.wheight-1)


	def do_layout_default(self):
		self.top_left_session_pane.set_position    (top_left_x=0,
		                                            top_left_y=1,
		                                            bottom_right_x=self.wwidth_left_end,
		                                            bottom_right_y=self.wheight_bottom_start)
		self.top_right_session_pane.set_position   (top_left_x=self.wwidth_right_start,
		                                            top_left_y=1,
		                                            bottom_right_x=self.wwidth,
		                                            bottom_right_y=self.wheight_bottom_start)
		self.bottom_right_session_pane.set_position(top_left_x=self.wwidth_right_start,
		                                            top_left_y=self.wheight_bottom_start,
		                                            bottom_right_x=self.wwidth,
		                                            bottom_right_y=self.wheight-1)
		self.bottom_left_session_pane.set_position (top_left_x=0,
		                                            top_left_y=self.wheight_bottom_start,
		                                            bottom_right_x=self.wwidth_left_end,
		                                            bottom_right_y=self.wheight-1)


# Represents a window pane with no concept of context or content.
class SessionPane(object):

	def __init__(self, name):
		self.name                 = name
		self.top_left_x           = -1
		self.top_left_y           = -1
		self.bottom_right_x       = -1
		self.bottom_right_y       = -1
		self.line_buffer_size     = 1000
		assert self.name in ('top_left','bottom_left','top_right','bottom_right')

	def __str__(self):
		string =  '\n============= SESSION PANE OBJECT BEGIN ==================='
		string += '\nname: '           + str(self.name)
		string += '\ntop_left_x: '     + str(self.top_left_x)
		string += '\ntop_left_y: '     + str(self.top_left_y)
		string += '\nbottom_right_x: ' + str(self.bottom_right_x)
		string += '\nbottom_right_y: ' + str(self.bottom_right_y)
		string += '\nwidth: '          + str(self.get_width())
		string += '\nheight: '         + str(self.get_width())
		string += '\n============= SESSION PANE OBJECT END   ==================='
		return string

	def set_position(self, top_left_x, top_left_y, bottom_right_x, bottom_right_y):
		self.top_left_x     = top_left_x
		self.top_left_y     = top_left_y
		self.bottom_right_x = bottom_right_x
		self.bottom_right_y = bottom_right_y

	def get_width(self):
		return self.bottom_right_x - self.top_left_x

	def get_height(self):
		return self.bottom_right_y - self.top_left_y


# Represents a line in the array of output
class SessionPaneLine(object):

	def __init__(self, line_str, time_seen, line_type):
		assert line_type in ('log','output')
		self.line_str        = line_str
		if isinstance(line_str, bytes):
			line_str = line_str.decode('utf-8')
		assert isinstance(line_str, unicode), 'line_str type: ' + str(type(line_str))
		self.time_seen       = time_seen
		self.time_seen       = time_seen

	def __str__(self):
		return self.line_str


def setup_signals():
	"""Set up the signal handlers.
	"""
	signal.signal(signal.SIGINT, shutit_util.ctrl_c_signal_handler)
	signal.signal(signal.SIGQUIT, shutit_util.ctrl_quit_signal_handler)


def get_shutit_pexpect_sessions():
	"""Returns all the shutit_pexpect sessions in existence.
	"""
	sessions = []
	for shutit_object in shutit_global_object.shutit_objects:
		for key in shutit_object.shutit_pexpect_sessions:
			sessions.append(shutit_object.shutit_pexpect_sessions[key])
	return sessions

shutit_global_object = ShutItGlobal()

# Only at this point can we import other modules, otherwise we get race failures.
from shutit_class import ShutIt, ShutItInit
import shutit_util

# Create default shutit object. TODO: do we need this?
shutit_global_object.shutit_objects.append(ShutIt(standalone=False, session_type='bash'))