from ..typecheck import *

import sublime
import sublime_plugin
import os
import subprocess
import re
import json
import types

from .. import core, ui, dap

from .autocomplete import Autocomplete

from .util import get_setting
from .config import PersistedData

from .debugger_session import (
	DebuggerSession,
	Variables,
	Watch,
	Terminals
)
from .debugger_sessions import (
	DebuggerSessions
)
from .debugger_project import (
	DebuggerProject
)
from .breakpoints import (
	Breakpoints,
)
from .adapter import (
	Configuration,
	ConfigurationExpanded,
	ConfigurationCompound,
	Adapters,
)
from .terminals import (
	Terminal,
	TerminalProcess,
	TermianlDebugger,
	TerminalView,
)
from .watch import WatchView

from .view_hover import ViewHoverProvider
from .view_selected_source import ViewSelectedSourceProvider
from .breakpoint_commands import BreakpointCommandsProvider

from .views.modules import ModulesView
from .views.sources import SourcesView
from .views.callstack import CallStackView

from .views.debugger_panel import DebuggerPanel
from .views.breakpoints_panel import BreakpointsPanel
from .views.variables_panel import VariablesPanel
from .views.tabbed_panel import TabbedPanel, TabbedPanelItem
from .views.selected_line import SelectedLine
from .debugger_log_output_panel import DebuggerLogOutputPanel


class Debugger:

	instances = {} #type: Dict[int, Debugger]
	creating = {} #type: Set[int, bool]

	@staticmethod
	def get(window: sublime.Window, run: bool = False) -> 'Optional[Debugger]':
		return Debugger.for_window(window, run)

	@staticmethod
	def should_auto_open_in_window(window: sublime.Window) -> bool:
		data = window.project_data()
		if not data:
			return False
		if "settings" not in data:
			return False
		if "debug.configurations" not in data["settings"]:
			return False
		return True

	@staticmethod
	def for_window(window: sublime.Window, create: bool = False) -> 'Optional[Debugger]':
		global instances
		id = window.id()
		instance = Debugger.instances.get(id)

		if not instance and create:
			if Debugger.creating.get(id):
				raise core.Error("We shouldn't be creating another debugger instance for this window...")

			Debugger.creating[id] = True
			try:
				instance = Debugger(window)
				Debugger.instances[id] = instance

			except dap.Error as e:
				core.log_exception()

			Debugger.creating[id] = False

		if instance and create:
			instance.show()

		return instance

	def __init__(self, window: sublime.Window) -> None:

		self.window = window
		self.disposeables = [] #type: List[Any]

		def on_project_configuration_updated():
			self.sessions.terminals.external_terminal_kind = self.project.external_terminal_kind
			self.configurations = self.project.configurations
			self.configuration = self.persistance.load_configuration_option(self.project.configurations, self.project.compounds)

		self.project = DebuggerProject(window)
		self.disposeables.append(self.project)
		self.project.on_updated.add(on_project_configuration_updated)
		
		self.transport_log = DebuggerLogOutputPanel(self.window)
		self.disposeables.append(self.transport_log)

		autocomplete = Autocomplete.create_for_window(window)

		def on_output(session:DebuggerSession, event: dap.OutputEvent) -> None:
			self.terminal.program_output(session, event)

		def on_terminal_added(terminal: Terminal):
			component = TerminalView(terminal, self.on_navigate_to_source)

			panel = TabbedPanelItem(id(terminal), component, terminal.name(), 0)
			def on_modified():
				... 
				#self.middle_panel.modified(panel)

			terminal.on_updated.add(on_modified)

			self.middle_panel.add(panel)
			self.middle_panel.select(id(terminal))

		def on_terminal_removed(terminal: Terminal):
			self.middle_panel.remove(id(terminal))

		self.breakpoints = Breakpoints()
		self.disposeables.append(self.breakpoints)
		
		self.sessions = DebuggerSessions()
		self.sessions.transport_log = self.transport_log
		self.sessions.output.add(on_output)

		self.sessions.terminals.on_terminal_added.add(on_terminal_added)
		self.sessions.terminals.on_terminal_removed.add(on_terminal_removed)
		self.disposeables.append(self.sessions)

		self.terminal = TermianlDebugger(
			on_run_command=self.on_run_command,
		)

		self.source_provider = ViewSelectedSourceProvider(self.project, self.sessions)
		self.view_hover_provider = ViewHoverProvider(self.project, self.sessions)
		self.breakpoints_provider = BreakpointCommandsProvider(self.project, self.sessions, self.breakpoints)
		self.disposeables.extend([self.view_hover_provider, self.source_provider, self.breakpoints_provider])

		self.persistance = PersistedData(self.project.name)
		self.load_data()
		on_project_configuration_updated()


		self.terminal.log_info('Opened In Workspace: {}'.format(os.path.dirname(self.project.name)))

		def on_terminal_updated():
			# self.panels.modified(terminal_panel_item)
			...
		self.terminal.on_updated.add(on_terminal_updated)

		#left panels
		self.breakpoints_panel = BreakpointsPanel(self.breakpoints)
		self.debugger_panel = DebuggerPanel(self, self.breakpoints_panel)

		# middle panels
		self.middle_panel = TabbedPanel([], 0)

		self.terminal_view = TerminalView(self.terminal, self.on_navigate_to_source)

		self.callstack_view =  CallStackView(self.sessions)
		self.middle_panel.update([
			TabbedPanelItem(self.terminal_view, self.terminal_view, 'Debugger Console'),
			TabbedPanelItem(self.callstack_view, self.callstack_view, 'Callstack'),
		])

		# right panels
		self.right_panel = TabbedPanel([], 0)

		self.variables_panel = VariablesPanel(self.sessions)
		self.modules_panel = ModulesView(self.sessions)
		self.sources_panel = SourcesView(self.sessions, self.source_provider.navigate)

		self.right_panel.update([
			TabbedPanelItem(self.variables_panel, self.variables_panel, 'Variables'),
			TabbedPanelItem(self.modules_panel, self.modules_panel, 'Modules'),
			TabbedPanelItem(self.sources_panel, self.sources_panel, 'Sources'),
		])
		self.update_modules_visibility()
		self.update_sources_visibility()

		# phantoms
		phantom_location = self.project.panel_phantom_location()
		phantom_view = self.project.panel_phantom_view()

		self.left = ui.Phantom(self.debugger_panel, phantom_view, sublime.Region(phantom_location, phantom_location), sublime.LAYOUT_INLINE)
		self.middle = ui.Phantom(self.middle_panel, phantom_view, sublime.Region(phantom_location + 0, phantom_location + 1), sublime.LAYOUT_INLINE)
		self.right = ui.Phantom(self.right_panel, phantom_view, sublime.Region(phantom_location + 0, phantom_location + 2), sublime.LAYOUT_INLINE)
		self.disposeables.extend([self.left, self.middle, self.right])

		self.sessions.on_updated_modules.add(lambda s: self.update_modules_visibility())
		self.sessions.on_updated_sources.add(lambda s: self.update_sources_visibility())
		self.sessions.on_removed_session.add(self.on_session_removed)
		self.sessions.updated.add(self.on_session_state_changed)
		self.sessions.on_selected.add(self.on_session_selection_changed)

	def on_session_removed(self, session: DebuggerSession):
		self.update_sources_visibility()
		self.update_modules_visibility()

	def on_session_selection_changed(self, session: DebuggerSession):
		if not self.sessions.has_active:
			self.source_provider.clear()
			return
		
		active_session = self.sessions.active
		thread = active_session.selected_thread
		frame = active_session.selected_frame

		if thread and frame and frame.source:
				self.source_provider.select(frame.source, frame.line, thread.stopped_reason or "Stopped")
		else:
			self.source_provider.clear()

	def on_session_state_changed(self, session: DebuggerSession, state):
		if state == DebuggerSession.stopped:
			if self.sessions or session.stopped_reason == DebuggerSession.stopped_reason_build_failed:
				... # leave build results open or there is still a running session
			else:
				self.show_console_panel()

		elif state == DebuggerSession.running:
			...
		elif state == DebuggerSession.paused:
			if self.project.bring_window_to_front_on_pause:
				# is there a better way to bring sublime to the front??
				# this probably doesn't work for most people. subl needs to be in PATH
				# ignore any errors
				try:
					subprocess.call(["subl"])
				except Exception:
					pass

			self.show_call_stack_panel()

		elif state == DebuggerSession.stopping or state == DebuggerSession.starting:
			...

	def update_sources_visibility(self):
		has_sources = False
		for session in self.sessions:
			if session.sources:
				has_sources = True
				break

		self.right_panel.set_visible(self.sources_panel, has_sources)

	def update_modules_visibility(self):
		has_modules = False
		for session in self.sessions:
			if session.modules:
				has_modules = True
				break

		self.right_panel.set_visible(self.modules_panel, has_modules)

	def show(self) -> None:
		self.project.panel_show()

	def show_console_panel(self) -> None:
		self.middle_panel.select(self.terminal_view)

	def show_call_stack_panel(self) -> None:
		self.middle_panel.select(self.callstack_view)

	def changeConfiguration(self, configuration: Union[Configuration, ConfigurationCompound]):
		self.configuration = configuration
		self.persistance.save_configuration_option(configuration)

	def dispose(self) -> None:
		self.save_data()
		for d in self.disposeables:
			d.dispose()

		del Debugger.instances[self.window.id()]

	def run_async(self, awaitable: Awaitable[core.T]):
		def on_error(e: Exception) -> None:
			self.terminal.log_error(str(e))
		core.run(awaitable, on_error=on_error)

	def on_navigate_to_source(self, source: dap.Source, line: Optional[int]):
		self.source_provider.navigate(source, line or 1)

	async def _on_play(self, no_debug=False) -> None:
		self.show_console_panel()
		self.sessions.terminals.clear_unused()
		self.terminal.clear()
		self.terminal.log_info('Console cleared...')
		try:
			if not self.configuration:
				self.terminal.log_error("Add or select a configuration to begin debugging")
				Adapters.select_configuration(self).run()
				return

			if isinstance(self.configuration, ConfigurationCompound):
				configurations = []
				for configuration_name in self.configuration.configurations:
					configuration = None
					for c in self.configurations:
						if c.name == configuration_name:
							configuration = c
							break

					if configuration:
						configurations.append(configuration)
					else:
						raise core.Error(f'Unable to find configuration with name {configuration_name} while evaluating compound {self.configuration.name}')

			elif isinstance(self.configuration, Configuration):
				configurations = [self.configuration]
			else: 
				raise core.Error('unreachable')
			

		except Exception as e:
			core.log_exception()
			core.display(e)
			return

		variables = self.project.extract_variables()

		for configuration in configurations:
			@core.schedule
			async def launch():
				try:
					adapter_configuration = Adapters.get(configuration.type)
					configuration_expanded = ConfigurationExpanded(configuration, variables)
					if not adapter_configuration.installed_version:
						install = 'Debug adapter with type name "{}" is not installed.\n Would you like to install it?'.format(adapter_configuration.type)
						if sublime.ok_cancel_dialog(install, 'Install'):
							await adapter_configuration.install(self)

					await self.sessions.launch(self.breakpoints, adapter_configuration, configuration_expanded, no_debug=no_debug)
				except core.Error as e:
					if sublime.ok_cancel_dialog("Error Launching Configuration\n\n{}".format(str(e)), 'Open Project'):
						self.project.open_project_configurations_file()

			launch()

	def is_paused(self):
		if not self.sessions.has_active:
			return False
		return self.sessions.active.state == DebuggerSession.paused

	def is_running(self):
		if not self.sessions.has_active:
			return False
		return self.sessions.active.state == DebuggerSession.running

	def is_stoppable(self):
		if not self.sessions.has_active:
			return False
		return self.sessions.active.state != DebuggerSession.stopped

	#
	# commands
	#
	def open(self) -> None:
		self.show()

	def quit(self) -> None:
		self.dispose()

	def on_play(self) -> None:
		self.show()
		self.run_async(self._on_play())

	def on_play_no_debug(self) -> None:
		self.show()
		self.run_async(self._on_play(no_debug=True))

	async def catch_error(self, awaitabe):
		try:
			return await awaitabe()
		except core.Error as e:  
			self.error(str(e))

	@core.schedule
	async def on_stop(self) -> None:
		await self.catch_error(lambda: self.sessions.active.stop())
	@core.schedule
	async def on_resume(self) -> None:
		await self.catch_error(lambda: self.sessions.active.resume())
	@core.schedule
	async def on_pause(self) -> None:
		await self.catch_error(lambda: self.sessions.active.pause())
	@core.schedule
	async def on_step_over(self) -> None:
		await self.catch_error(lambda: self.sessions.active.step_over())
	@core.schedule
	async def on_step_in(self) -> None:
		await self.catch_error(lambda: self.sessions.active.step_in())
	@core.schedule
	async def on_step_out(self) -> None:
		await self.catch_error(lambda: self.sessions.active.step_out())
	@core.schedule
	async def on_run_command(self, command: str) -> None:
		await self.catch_error(lambda: self.sessions.active.evaluate(command))

	def on_input_command(self) -> None:
		label = "Input Debugger Command"
		def run(value: str):
			if value:
				self.run_async(self.sessions.active.evaluate(value))
				self.on_input_command()

		input = ui.InputText(run, label, enable_when_active=Autocomplete.for_window(self.window))
		input.run()

	def toggle_breakpoint(self):
		self.breakpoints_provider.toggle_current_line()

	def toggle_column_breakpoint(self):
		self.breakpoints_provider.toggle_current_line_column()

	def add_function_breakpoint(self):
		self.breakpoints.function.add_command()

	def add_watch_expression(self):
		self.sessions.watch.add_command()

	def run_to_current_line(self) -> None:
		self.breakpoints_provider.run_to_current_line()

	def load_data(self):
		self.breakpoints.load_from_json(self.persistance.json.get('breakpoints', {}))
		self.sessions.watch.load_json(self.persistance.json.get('watch', []))

	def save_data(self):
		self.persistance.json['breakpoints'] = self.breakpoints.into_json()
		self.persistance.json['watch'] = self.sessions.watch.into_json()
		self.persistance.save_to_file()

	def on_settings(self) -> None:
		import webbrowser
		def about():
			webbrowser.open_new_tab("https://github.com/daveleroy/sublime_debugger/blob/master/docs/setup.md")
		
		def report_issue():
			webbrowser.open_new_tab("https://github.com/daveleroy/sublime_debugger/issues")

		values = Adapters.select_configuration(self).values
		values.extend([
			ui.InputListItem(lambda: ..., ""),
			ui.InputListItem(report_issue, "Report Issue"),
			ui.InputListItem(about, "About/Getting Started"),
		])

		ui.InputList(values).run()

	def install_adapters(self) -> None:
		self.show_console_panel()
		Adapters.install_menu(log=self).run()

	def change_configuration(self) -> None:
		Adapters.select_configuration(self).run()

	def error(self, value: str):
		self.terminal.log_error(value)

	def info(self, value: str):
		self.terminal.log_info(value)

	def refresh_phantoms(self) -> None:
		ui.reload()