''' Root widget of the application which handles drawing itself and 
    loading components. Most elements are defined declaratively in 
    gui.json in the resources dir.
'''

import os
import json

from functools import partial
from collections import OrderedDict
from loguru import logger
from pathlib import Path

from kivymd.uix.filemanager import MDFileManager
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang import Builder

from jocular.widgets.widgets import (
    JLabel, JToggleButton, 
    JIconButton, JButton
    )
from jocular.widgets.jlever import JLever
from jocular.widgets.icons import get_icon
from jocular.uranography import angle360
from jocular.component import Component
from jocular.metrics import Metrics
from jocular.ring import Ring
from jocular.utils import get_datadir, start_logging, add_if_not_exists


Builder.load_string('''

<Splash>:
    canvas:
        Color:
            rgba: .2, .2, .2, 0
        Rectangle:
            pos: self.pos
            size: self.width, self.height
    padding: 20, 20
    orientation: 'vertical'
    size_hint: None, None
    size: dp(450), dp(450)
    pos_hint: {'center_x': 10, 'center_y': .5}
    message: _message 
    Label:
        text: 'Welcome to J[font=Jocular][size=60]o[/size][/font]cular!'
        font_size: '40sp'
        markup: True
    Label:
        halign: 'center'
        valign: 'middle'
        text_size: self.size
        font_size: '20sp'
        text: 'Before you can get started, Jocular needs a place to store captures, calibration files, observations and the like.'
    Label:
        text: ''
    Button:
        size_hint: 
        text: 'Choose Jocular data directory ...'
        font_size: '18sp'
        on_press: root.choose_dir()
    Label:
        id: _message
        text: ''

''')


class Splash(BoxLayout):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.app = App.get_running_app()
 
    def choose_dir(self, *args):
        ''' create filemanager and open in user's home directory
        '''
        fm = MDFileManager()
        fm.exit_manager = partial(self.exit_filemanager, fm)
        fm.select_path = partial(self.handle_selection, fm)
        fm.show(str(Path.home()))

    def exit_filemanager(self, fm):
        fm.close()

    def handle_selection(self, fm, datadir):

        # timestamp in datadir to check that it is writeable
        try:
            with open(os.path.join(datadir, '.canwrite'), 'w') as f:
                f.write('ok')
        except:
            self.message.text = 'Cannot write to that directory'
            return

        # store absolute path to datadir in .jocular if possible
        try:
            with open(os.path.join(str(Path.home()), '.jocular'), 'w') as f:
                f.write(datadir)
        except:
            self.message.text = 'Cannot write .jocular to home directory'
            return

        # store jocular data dir in main app
        self.app.data_dir = datadir

        # start logging
        start_logging(self.app.get_path('logs'))

        # close filemanager and splashscreen
        fm.close()
        self.hide()

        # if there isn't already a captures/examples directory
        # create one and move example captures to it
        try:
            captures = os.path.join(datadir, 'captures')
            add_if_not_exists(captures)
            # shutil.move(
            #     self.app.get_path('example_captures'), 
            #     captures)
        except Exception as e:
            logger.warning(f'Problem creating captures directory ({e})')

        # and run next steps
        
        Clock.schedule_once(self.app.gui.load_components, 0)

    def show(self, *args):
        self.pos_hint = {'center_x': .5, 'center_y': .5}       

    def hide(self, *args):
        self.pos_hint = {'center_x': 10, 'center_y': .5}       


def _true(val):
    if type(val) == bool:
        return val
    if type(val) == int:
        return val == 1
    if type(val) == str:
        return val.lower() in ['1', 'true', 'yes']
    return False


class GUI(FloatLayout):


    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.app = App.get_running_app()
        self.disabled_controls = set({})
        self.gui = OrderedDict()
        self.bind(size=self.redraw)

        # dictionary recording all components that have changed
        self.changed_components = {}

        # load gui settings or initialise to empty if any issues
        try:
            with open(self.app.get_path('gui_settings.json'), 'r') as f:
                self.config = json.load(f)
        except:
            self.config = {}


    def draw(self, dt=None):

        # construct interface for the first time; we redraw in the order originally drawn
        orig_gui = OrderedDict()

        # Component.get('View')
        reticle = Component.get('Reticle')

        # handle rings first due to draw order
        origin = Metrics.get('origin')
        radii = Metrics.get('ring_radius')
        thickness = Metrics.get('ring_thickness')

        greys = {'background': .02, 'outer': .2, 'middle': .17, 'inner': .12, 'image': 0} # , 'capture_ring': .2}
        for nm, r in radii.items():
            if nm != 'image':
                orig_gui[nm] = {
                    'control_type': 'Ring',
                    'widget': Ring(pos=origin, radius=r, thickness=thickness[nm], grey=greys[nm],
                        start_angle=0, end_angle=360)
                    } 

        # focus/alignment reticle
        orig_gui['reticle'] = {
            'control_type': 'Reticle',
            'widget': reticle
        }
 
        # load GUI spec and convert so widget names are keys
        with open(self.app.get_path('gui.json'), 'r') as f:
            gs = json.load(f)

        for comp, widgets in gs.items():
            for name, widget_spec in widgets.items():
                orig_gui[name] = widget_spec
                orig_gui[name]['component'] = comp

        self.gui = OrderedDict()
        # set up a variable to use for new elements such as ring annotations which need a unique name
        self.new_element_count = 0
        for name, spec in orig_gui.items():
            self.draw_element(name, spec)
            spec['widget'].disabled = True

        self.constructed = True

        # check if we have a data dir
        try:
            data_dir = get_datadir()
            if data_dir is None:
                self.make_splash()
            else:
                self.app.data_dir = data_dir
                Clock.schedule_once(self.load_components, 0)
                logger.info('GUI drawn')
        except Exception as e:
            logger.exception(e)


    def make_splash(self):
        ''' put up welcome screen and get hold of datadir, then load components
        '''
        Window.size = 1200, 800
        s = Splash()
        self.add_widget(s)
        s.show()


    def load_components(self, dt=None):

        for name, spec in self.gui.items():
            spec['widget'].disabled = False

        logger.info('starting to load components')

        for c in ['Status', 'Appearance', 'Metadata', 'Catalogues', 
            'Notes', 'DSO', 'Session', 'Stacker', 
            'Capture', 'CaptureScript', 'Observations', 'ObservingList', 
            'Monochrome', 'MultiSpectral', 'ObjectIO', 'Aligner',
            'DeviceManager', 'Camera', 'FilterWheel', 
            'ExposureChooser',  'FilterChooser', 
            'SettingsManager', 'Stretcher', 'StackCombiner',
            'BadPixelMap', 'Calibrator', 'Snapshotter', 'PlateSolver', 
            'Annotator', 'Help']:
            Component.get(c)

        # bind status to components
        Component.bind_status()

        logger.info('all components loaded')

        # prepare for new object
        Component.initialise_new_object()
        logger.info('new object initialised')


    def redraw(self, *args):
        ''' Redraw interface by relocating components
        '''

        # if still being constructed, do nothing
        if not hasattr(self, 'constructed'):
            return

        origin = Metrics.get('origin')
        ring_radius = Metrics.get('ring_radius')
        thickness = Metrics.get('ring_thickness')
 
        for name, spec in self.gui.items():
            if 'widget' in spec:
                w = spec['widget']
                if spec['control_type'] == 'Ring' and name in ring_radius:
                    w.relocate(origin=origin, radius=ring_radius[name], thickness=thickness[name])
                elif spec['control_type'] == 'Reticle':
                    w.pos = origin
                    w.radius = ring_radius['image']
                else:
                    w.relocate(origin=origin, radius=Metrics.get(spec['location']))


    def draw_element(self, wname, spec):
        ''' Draw element and add to gui dictionary for later redrawing
        '''

        control_type = spec['control_type']

        if control_type in {'Ring', 'Reticle'}:
            self.add_widget(spec['widget'])
            self.gui[wname] = spec 
            return

        angle = spec['angle']
        location = spec['location']
        radial = spec.get('radial', False)
        tooltip = spec.get('tooltip', '')

        # get initial value from config, else from initial, else make it up

        if spec.get('group', '-') in self.config:
            initial_value = wname == self.config[spec['group']]
        elif wname in self.config:
            initial_value = self.config[wname]
        elif 'initial' in spec:
            initial_value = spec['initial']
        elif 'values' in spec:
            initial_value = spec['values'][0]
        else:
            initial_value = 0

        # ensure that initial value is within range
        if 'values' in spec:
            if initial_value > spec['values'][1]:
                initial_value = spec['values'][1]
                logger.debug(f'resetting initial_value of {wname} to {initial_value}')
            elif initial_value < spec['values'][0]:
                initial_value = spec['values'][0]
                logger.debug(f'resetting initial_value of {wname} to {initial_value}')
            # print(f'checking {initial_value} is in {spec["values"]}')

        rad = Metrics.get(location)
        orig = Metrics.get('origin')
        radial = spec.get('radial', False)

        # handle icons uniformly
        if 'icon' in spec and 'text' in spec:
            text = get_icon(
                spec['icon'], 
                font_size=spec.get('font_size', None)) + '\n' + spec['text']  
        elif 'icon' in spec:
            text = get_icon(
                spec['icon'], 
                font_size=spec.get('font_size', None))
        else:
            text = spec.get('text', '')

        if control_type == 'JToggleButton':
            w = JToggleButton(angle=angle, origin=orig, radius=rad, text=text, radial=radial,
                allow_no_selection=False)
            if 'group' in spec:
                w.group = spec['group']
            w.state = 'down' if _true(initial_value) else 'normal'

        elif control_type == 'JButton':
            w = JButton(angle=angle, origin=orig, radius=rad, radial=radial, 
                text=text)

        # to do: see if we can combine with JButton
        elif control_type == 'JIconButton':
            w = JIconButton(angle=angle, origin=orig, radius=rad, myicon=text)

        elif control_type == 'JLabel':
            w = JLabel(angle=angle, origin=orig, radius=rad, text=spec.get('text', ''))

        elif control_type == 'JLever':

            if spec.get('groove', False):
                min_a, max_a = angle360(90 - angle[0]), angle360(90 - angle[1])
                w = Ring(pos=orig, radius=rad, thickness=Metrics.get('outer_width')/2,
                    grey=.13, start_angle=angle360(min(min_a, max_a)), 
                    end_angle=angle360(max(min_a, max_a)))
                self.gui[f'_new{self.new_element_count}'] = {
                    'control_type': 'Ring', 
                    'widget': w,
                    'save': False,
                    'location': location}
                self.new_element_count += 1
                self.add_widget(w)

            w = JLever(angles=angle, origin=orig, radius=rad, value=initial_value, 
                    values=spec['values'],
                    continuous_update=spec.get('continuous', None),
                    shortcut=spec.get('shortcut', None),
                    sc_inc=spec.get('sc_inc', None),
                    text=spec.get('text', get_icon('lever', font_size=19)),
                    radial=spec.get('radial', True))

            self.add_widget(w)  # added here to get depth order right

            if spec.get('major', False):
                for vstr, v in spec['major'].items():
                    lab = JLabel(
                        radial=spec.get('radial', True), text=vstr, 
                        angle=w.value_to_angle(v), origin=orig, radius=rad)
                    self.add_widget(lab) 
                    self.gui[f'_new{self.new_element_count}'] = {
                        'control_type': 'JLabel', 
                        'widget': lab,
                        'save': False,
                        'location': location}
                    self.new_element_count += 1

        # post inits (may need more)
        if not 'icon' in spec:
            for f in ['font_size', 'color', 'text_width', 'background_color', 'halign', 'radial']:
                if f in spec:
                    setattr(w, f, spec[f])
  
        if control_type == 'JLever':
            w.bind(value=partial(self.on_action, wname))
        else:
            self.add_widget(w)
            w.bind(on_press=partial(self.on_action, wname))

        w.tooltip_text = tooltip
        spec['widget'] = w
        self.gui[wname] = spec


    def initialise_component(self, component):
        ''' Push initial settings into the component when it is loaded
        '''

        for k, spec in self.gui.items():
            if spec.get('component', None) == component:
                # do NOT do actions!
                if 'action' not in spec:
                    if 'group' in spec:
                        if spec['widget'].state == 'down':
                            self.on_action(k)
                    else:
                        self.on_action(k)


    def get_gui_setting(self, key):
        if hasattr(self, 'config'):
            return self.config.get(key, None)
        return None


    def set(self, name, value, update_property=False):
        ''' Called by any component that needs to access GUI element, usually
            for something simple like changing text. Option to pass this
            change back to component via update property
        '''

        if name not in self.gui:
            return
        spec = self.gui[name]
        ctype = spec['control_type']
        if ctype in ['JLabel', 'JButton', 'JIconButton']:
            setattr(spec['widget'], 'text', str(value))
        elif ctype == 'JLever':
            getattr(spec['widget'], 'reset_value')(value)
        elif ctype == 'JToggleButton':
            # if it is a group, set all but this to normal
            if 'group' in spec:
                gname = spec['group']
                for n, v in self.gui.items():
                    if v.get('group','') == gname:
                        v['widget'].state = 'down' if n == name else 'normal'
            else:
                setattr(spec['widget'], 'state', 'down' if value else 'normal')
        if update_property:
            self.on_action(name)


    def set_prop(self, name, prop, value):
        if name not in self.gui:
            return
        spec = self.gui[name]
        if prop == 'text' and spec['control_type'] == 'JLever':
            spec['widget'].text = value
        else:
            if hasattr(spec, prop):
                setattr(spec, prop, value)


    def get(self, name):
        ''' Access value of a given component; occasionally required
        '''

        if name not in self.gui:
            return None
        spec = self.gui[name]
        w = spec['widget']
        ctype = spec['control_type']
        if ctype in ['JLabel', 'JButton']:
            return w.text
        if ctype == 'JLever':
            return w.value
        if ctype == 'JToggleButton':
            return 1 if w.state == 'down' else 0
        return None


    def on_action(self, name, *args):
        ''' Intercept any GUI event in order to load component, before
            passing action on to component via a property change
        '''

        spec = self.gui[name]
        comp = spec['component']
        c = Component.get(comp)  # loads if needed

        if name in self.disabled_controls:
            return

        # if action is specified, simply call that method on component
        if 'action' in spec:
            getattr(c, spec['action'])()
            return

        # otherwise it involves a value change
        control = spec['control_type']

        # handle button groups
        if control == 'JToggleButton':
            if 'group' in spec:
                setattr(c, spec['group'], spec['widget'].text)
                return

        if hasattr(c, name):
            if control == 'JToggleButton':
                setattr(c, name, spec['widget'].state == 'down')
            elif control == 'JLever':
                setattr(c, name, spec['widget'].value)
            # elif control == 'JMulti':
            #     setattr(c, name, spec['widget'].text)


    def enable(self, names=None):
        # None indicates enable everything
        if names is None:
            names = self.disabled_controls
        # remove names from disabled list
        self.disabled_controls = self.disabled_controls - set(names)
        for n in names:
            if n in self.gui:
                spec = self.gui[n]
                spec['widget'].disabled = False


    def disable(self, names):
        # adds names to disabled list
        self.disabled_controls = self.disabled_controls.union(set(names))
        for n in names:
            if n in self.gui:
                spec = self.gui[n]
                spec['widget'].disabled = True


    def on_close(self, *args):
        ''' Save certain GUI values to config. We save everything except
            actions and elements whose save property is False
        '''
        pass # actually done in Component now


    ''' Leaving this here por si acaso
        uses plyer, but that is super-ugly on Windows, but keep code in case
        kivymd filemanager breaks down....
    '''

    # def choose_dir(self, *args):
    #   filechooser.choose_dir(
    #       on_selection=self._handle_selection,
    #       multiple=False)

    # def handle_selection(self, selection):
    #   datadir = os.path.abspath(selection[0])

    #   # timestamp in datadir to check that it is writeable
    #   try:
    #       with open(os.path.join(datadir, '.canwrite'), 'w') as f:
    #           f.write('ok')
    #   except Exception as e:
    #       self.message.text = 'Cannot write to that directory'
    #       return

    #   # store absolute path to datadir in .jocular if possible
    #   try:
    #       with open(os.path.join(str(Path.home()), '.jocular'), 'w') as f:
    #           f.write(datadir)
    #   except Exception as e:
    #       self.message.text = 'Cannot write .jocular to home directory'
    #       return

    #   # store jocular data dir in main app
    #   App.get_running_app().data_dir = datadir

    #   # and close panel
    #   self.hide()

    #   # and run next steps
    #   Clock.schedule_once(self.gui.load_components, 0)

