# -*- coding: utf-8 -*-

from __future__ import print_function
import random
import zmq
import sys
import time
import json
import importlib
import imp
import abc
import pytz
from pdb import Pdb
from datetime import datetime
from six import with_metaclass
from zmq.eventloop import ioloop, zmqstream
import linecache
import multiprocessing

# from keystone.app_client.client import StrategyClient, byteify
from keystone.strategy import KSStrategy, KSStrategyRunner
from keystone.sources import KSCsvSource
from keystone.performance.analyzer import KSDefaultAnalyzer, KSCumulativeAnalyzer
from keystone.app_client import zmq_utils 
from keystone.utils.json_utils import json_clean, UTC_TIMESTAMP
from keystone.app_client.datetime_breakpoint import DatetimeBreakpoint
from keystone.py3compat import safe_unicode, PY3

KPDB_NOTIFY_REACH_BREAKPOINT = "kpdb_notify_reach_breakpoint"
KPDB_SET_BREAKPOINT_REPLY = "kpdb_set_breakpoint_reply"

KPDB_MSG_TYPE = [KPDB_NOTIFY_REACH_BREAKPOINT, KPDB_SET_BREAKPOINT_REPLY]

class KPdb(Pdb):
	def __init__(self, 
		filename,
		session, 
		notify_url, 
		syn_url, 
		datetime_break_func=None,
		datetime_break_cond=None,
		current_time_func=None,
		completekey='tab', stdin=None, stdout=None, skip=None):
		Pdb.__init__(self, completekey, stdin, stdout, skip)
		self.filename = filename
		self.stop_flag = True
		self.lock = multiprocessing.Lock() 
		self.init_notify_sock(notify_url, syn_url)
		self.session = session
		self.datetime_break_func = datetime_break_func
		self.datetime_break_cond = datetime_break_cond
		self.current_time_func = current_time_func
		# set datetime break condition
		if self.datetime_break_func is not None:
			self._break_at_function(self.datetime_break_func, cond=self.datetime_break_cond)

	# overide step_here,用于限制用户只能停留在当前文件中
	def stop_here(self, frame):
		fname = self.canonic(frame.f_code.co_filename)
		if fname != self.filename:
			return False
		return Pdb.stop_here(self,frame);

	def init_notify_sock(self, notify_url, syn_url):
		self.notify_socket = zmq_utils.create_publisher_socket(notify_url, syn_url)

	def notify_frontend(self, msg_type, content):
		assert msg_type in KPDB_MSG_TYPE

		content = json_clean(content)
		msg = self.session.send(self.notify_socket, safe_unicode(msg_type), content = content, ident = b'stream.kpdb_notify')

	def forget(self):
		with self.lock:
			self.stop_flag = False
		DatetimeBreakpoint.hit_datetime_breakpoint = None
		Pdb.forget(self)

	@property
	def is_running(self):
		with self.lock:
			is_stop = self.stop_flag
		return not is_stop

	def construct_notify_message(self, frame):
		fname = self.canonic(frame.f_code.co_filename)
		lineno = frame.f_lineno
		if DatetimeBreakpoint.hit_datetime_breakpoint is None:
			content = json_clean(dict(type='line_breakpoint', filename=fname, lineno=lineno))
		else:
			content = json_clean(dict(
				type='datetime_breakpoint', 
				datetime=DatetimeBreakpoint.hit_datetime_breakpoint, 
				filename=fname, 
				lineno=lineno,
				datetime_format=UTC_TIMESTAMP))
		return content

	def user_return(self, frame, return_value):
		with self.lock:
			self.stop_flag = True
		# construct notify message
		# content = self.construct_notify_message(frame)
		# notify frontend
		# self.notify_frontend(msg_type=KPDB_NOTIFY_REACH_BREAKPOINT, content=content)
		Pdb.user_return(self, frame, return_value)
		
	def user_call(self, frame, argument_list):
		with self.lock:
			self.stop_flag = True
		# construct notify message
		# content = self.construct_notify_message(frame)
		# notify frontend
		# self.notify_frontend(msg_type=KPDB_NOTIFY_REACH_BREAKPOINT, content=content)
		Pdb.user_call(self, frame, argument_list)

	def user_line(self, frame):
		with self.lock:
			self.stop_flag = True
		# construct notify message
		content = self.construct_notify_message(frame)
		# notify frontend
		self.notify_frontend(msg_type=KPDB_NOTIFY_REACH_BREAKPOINT, content=content)
		# call Pdb.user_line
		Pdb.user_line(self, frame)

	''' custom clear_all function without interaction with user '''
	def do_clear_all(self, arg):
		self.clear_all_breaks()
		DatetimeBreakpoint.clear()
		# restore datetime break condition
		if self.datetime_break_func is not None:
			self._break_at_function(self.datetime_break_func, cond=self.datetime_break_cond)

	''' override original do_clear function '''
	def do_clear(self, arg):
		if not arg:
			DatetimeBreakpoint.clear()
		Pdb.do_clear(self,arg)
		# restore datetime break condition
		if self.datetime_break_func is not None:
			self._break_at_function(self.datetime_break_func, cond=self.datetime_break_cond)

	def do_break_line(self, arg, temporary = 0):
		filename = None
		lineno = None
		colon = arg.rfind(':')
		if colon >= 0:
			filename = arg[:colon].rstrip()
			arg = arg[colon+1:].lstrip()
			try:
				lineno = int(arg)
				err = self._break_at_line(filename, lineno, temporary)
				if err:
					pass
					# print >>sys.stderr, err
					# return
			except ValueError as msg:
				print('***(KPdb) Bad lineno:', arg, file=sys.stderr)
				return
		else:
			print("***(KPdb) Bad arguments: ", arg, file=sys.stderr)

	def do_unbreak_line(self, arg, temporary = 0):
		filename = None
		lineno = None
		colon = arg.rfind(':')
		if colon >= 0:
			filename = arg[:colon].rstrip()
			arg = arg[colon+1:].lstrip()
			try:
				lineno = int(arg)
				err = self.clear_break(filename, lineno)
				if err:
					pass
					# print >>sys.stderr, err
					# return
			except ValueError as msg:
				print('***(KPdb) Bad lineno:', arg, file=sys.stderr)
				return
		else:
			print("***(KPdb) Bad arguments: ", arg, file=sys.stderr)


	def do_break_datetime(self, arg, temporary = 0):
		# print >>sys.stdout, "this is KPdb break_datetime"
		# print >>sys.stdout, arg
		try:
			timestamps = arg.split(',')
			for ts in timestamps:
				dt = datetime.utcfromtimestamp(float(ts))
				current_dt = None
				if self.current_time_func is not None:
					current_dt = self.current_time_func()

				if current_dt is None or dt > current_dt:
					DatetimeBreakpoint.datetime_breakpoints[dt] = False
				# print >>sys.stdout, "break point: ",
				# print >>sys.stdout, dt
		except Exception as e:
			print(e, file=sys.stderr)

	def do_unbreak_datetime(self, arg):
		# print >>sys.stdout, "this is KPdb unbreak_datetime"
		# print >>sys.stdout, arg
		try:
			timestamps = arg.split(',')
			for ts in timestamps:
				dt = datetime.utcfromtimestamp(float(ts))
				if dt in DatetimeBreakpoint.datetime_breakpoints:
					del DatetimeBreakpoint.datetime_breakpoints[dt]
		except Exception as e:
			print(e, file=sys.stderr) 

	def _break_at_line(self, filename, lineno, temporary = 0, cond = None):
		funcname = None
		filename = filename.rstrip()
		f = self.lookupmodule(filename)
		if not f:
			return '***(KPdb) ' + repr(filename) + 'not found'
		else:
		    filename = f
		try:
		    lineno = int(lineno)
		except ValueError as msg:
		    return '***(KPdb) Bad lineno:' + str(lineno)

		# now check line
		err = self._checkline(filename, lineno)
		if err:
			return '***(KPdb) ' + err
		# now set the break point
		err = self.set_break(filename, lineno, temporary, cond, funcname)
		if err: 
			return '***(KPdb) ' + err
		# bp = self.get_breaks(filename, line)[-1]
		# print >>self.stdout, "Breakpoint %d at %s:%d" % (bp.number,
		#                                                  bp.file,
		#                                                  bp.line)

	def _break_at_function(self, arg, temporary = 0, cond = None):
		try:
		    func = eval(arg, self.curframe.f_globals, self.curframe_locals)
		except:
		    func = arg
		try:
			if hasattr(func, 'im_func'):
				if PY3:
					func = func.__func__
				else:
					func = func.im_func
			if PY3:
				code = func.__code__
			else:
				code = func.func_code
			#use co_name to identify the bkpt (function names
			#could be aliased, but co_name is invariant)
			funcname = code.co_name
			lineno = code.co_firstlineno
			filename = code.co_filename
		except:
		    # last thing to try
		    (ok, filename, ln) = self.lineinfo(arg)
		    if not ok:
		        return '***(KPdb) The specified object' + repr(arg) + 'is not a function or was not found along sys.path.'

		    funcname = ok # ok contains a function name
		    lineno = int(ln)

		# now check line
		err = self._checkline(filename, lineno)
		if err:
			return '***(KPdb) ' + err
		# now set the break point
		err = self.set_break(filename, lineno, temporary, cond, funcname)
		if err: 
			return '***(KPdb) ' + err

	def _checkline(self, filename, lineno):
		# Check whether specified line seems to be executable.
		globs = self.curframe.f_globals if hasattr(self, 'curframe') and self.curframe is not None else None
		line = linecache.getline(filename, lineno, globs)
		if not line:
		    return '***(KPdb) End of file'
		line = line.strip()
		# Don't allow setting breakpoint at a blank line
		if (not line or (line[0] == '#') or
		     (line[:3] == '"""') or line[:3] == "'''"):
		    return '***(KPdb) Blank or comment'



