using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Input;
using System.Windows.Data;
using System.Windows.Shapes;
using System.Windows.Media.Imaging;
using System.ComponentModel;
using System.Collections.Generic;

namespace smx_config
{
    // The checkbox to enable and disable the advanced per-panel sliders.
    //
    // This is always enabled if the thresholds in the configuration are set to different
    // values.  If the user enables us, we'll remember that we were forced on.  If the user
    // disables us, we'll sync the thresholds back up and turn the ForcedOn flag off.
    public class AdvancedThresholdViewCheckbox: CheckBox
    {
        public static readonly DependencyProperty AdvancedModeEnabledProperty = DependencyProperty.Register("AdvancedModeEnabled",
            typeof(bool), typeof(AdvancedThresholdViewCheckbox), new FrameworkPropertyMetadata(false));
        public bool AdvancedModeEnabled {
            get { return (bool) GetValue(AdvancedModeEnabledProperty); }
            set { SetValue(AdvancedModeEnabledProperty, value); }
        }

        OnConfigChange onConfigChange;

        // If true, the user enabled advanced view and we should display it even if
        // the thresholds happen to be synced.  If false, we'll only show the advanced
        // view if we need to because the thresholds aren't synced.
        bool ForcedOn;
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
                LoadUIFromConfig(ActivePad.GetFirstActivePadConfig(args));
            });
        }

        private void LoadUIFromConfig(SMX.SMXConfig config)
        {
            // The master version doesn't actually matter, but we use this as a signal that the panels
            // have a new enough firmware to support this.
            bool SupportsAdvancedMode = config.masterVersion != 0xFF && config.masterVersion >= 2;
            Visibility = SupportsAdvancedMode? Visibility.Visible:Visibility.Collapsed;

            // If the thresholds are different, force the checkbox on.  This way, if you load the application
            // with a platform with per-panel thresholds, and change the thresholds to no longer be different,
            // advanced mode stays forced on.  It'll only turn off if you uncheck the box, or if you exit
            // the application with synced thresholds and then restart it.
            if(SupportsAdvancedMode && !ConfigPresets.AreUnifiedThresholdsSynced(config))
                ForcedOn = true;

            // Enable advanced mode if the master says it's supported, and either the user has checked the
            // box to turn it on or the thresholds are different in the current configuration.
            AdvancedModeEnabled = SupportsAdvancedMode && ForcedOn;
        }

        protected override void OnClick()
        {
            if(AdvancedModeEnabled)
            {
                // Stop forcing advanced mode on, and sync the thresholds so we exit advanced mode.
                ForcedOn = false;

                foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
                {
                    int pad = activePad.Item1;
                    SMX.SMXConfig config = activePad.Item2;
                    ConfigPresets.SyncUnifiedThresholds(ref config);
                    SMX.SMX.SetConfig(pad, config);
                }
                CurrentSMXDevice.singleton.FireConfigurationChanged(this);
            }
            else
            {
                // Enable advanced mode.
                ForcedOn = true;
            }

            // Refresh the UI.
            LoadUIFromConfig(ActivePad.GetFirstActivePadConfig());
        }
    }

    // This implements the threshold slider widget for changing an upper/lower threshold pair.
    public class ThresholdSlider: Control
    {
        public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon",
            typeof(ImageSource), typeof(ThresholdSlider), new FrameworkPropertyMetadata(null));

        public ImageSource Icon {
            get { return (ImageSource) GetValue(IconProperty); }
            set { SetValue(IconProperty, value); }
        }

        public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type",
            typeof(string), typeof(ThresholdSlider), new FrameworkPropertyMetadata(""));

        public string Type {
            get { return (string) GetValue(TypeProperty); }
            set { SetValue(TypeProperty, value); }
        }

        public static readonly DependencyProperty AdvancedModeEnabledProperty = DependencyProperty.Register("AdvancedModeEnabled",
            typeof(bool), typeof(ThresholdSlider), new FrameworkPropertyMetadata(false, RefreshAdvancedModeEnabledCallback));

        public bool AdvancedModeEnabled {
            get { return (bool) GetValue(AdvancedModeEnabledProperty); }
            set { SetValue(AdvancedModeEnabledProperty, value); }
        }

        private static void RefreshAdvancedModeEnabledCallback(DependencyObject target, DependencyPropertyChangedEventArgs args)
        {
            ThresholdSlider self = target as ThresholdSlider;
            self.RefreshVisibility();
        }

        DoubleSlider slider;
        Label LowerLabel, UpperLabel;
        Image ThresholdWarning;

        OnConfigChange onConfigChange;

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            slider = GetTemplateChild("Slider") as DoubleSlider;
            LowerLabel = GetTemplateChild("LowerValue") as Label;
            UpperLabel = GetTemplateChild("UpperValue") as Label;
            ThresholdWarning = GetTemplateChild("ThresholdWarning") as Image;

            slider.ValueChanged += delegate(DoubleSlider slider) { SaveToConfig(); };

            onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
                LoadUIFromConfig(ActivePad.GetFirstActivePadConfig(args));
            });
        }

        Dictionary<string, int> panelNameToIndex = new Dictionary<string, int>() {
            { "up-left",    0 },
            { "up",         1 },
            { "up-right",   2 },
            { "left",       3 },
            { "center",     4 },
            { "right",      5 },
            { "down-left",  6 },
            { "down",       7 },
            { "down-right", 8 },

            // The cardinal and corner sliders write to the down and up-right panels, and
            // are then synced to the other panels by SyncUnifiedThresholds.
            { "cardinal",   7 },
            { "corner",     2 },
        };
        private void SetValueToConfig(ref SMX.SMXConfig config)
        {
            int panelIdx = panelNameToIndex[Type];
            if(!config.fsr())
            {
                byte lower = (byte) slider.LowerValue;
                byte upper = (byte) slider.UpperValue;
                config.panelSettings[panelIdx].loadCellLowThreshold = lower;
                config.panelSettings[panelIdx].loadCellHighThreshold = upper;
            } else {
                byte lower = (byte) slider.LowerValue;
                byte upper = (byte) slider.UpperValue;
                for(int sensor = 0; sensor < 4; ++sensor)
                {
                    config.panelSettings[panelIdx].fsrLowThreshold[sensor] = lower;
                    config.panelSettings[panelIdx].fsrHighThreshold[sensor] = upper;
                }
            }

            // If we're not in advanced mode, sync the cardinal value to each of the panel values.
            if(!AdvancedModeEnabled)
                ConfigPresets.SyncUnifiedThresholds(ref config);
        }

        private void GetValueFromConfig(SMX.SMXConfig config, out int lower, out int upper)
        {
            int panelIdx = panelNameToIndex[Type];

            if(!config.fsr())
            {
                lower = config.panelSettings[panelIdx].loadCellLowThreshold;
                upper = config.panelSettings[panelIdx].loadCellHighThreshold;
            } else {
                lower = config.panelSettings[panelIdx].fsrLowThreshold[0];
                upper = config.panelSettings[panelIdx].fsrHighThreshold[0];
            }
        }

        private void SaveToConfig()
        {
            if(UpdatingUI)
                return;

            // Apply the change and save it to the devices.
            foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
            {
                int pad = activePad.Item1;
                SMX.SMXConfig config = activePad.Item2;

                SetValueToConfig(ref config);
                SMX.SMX.SetConfig(pad, config);
                CurrentSMXDevice.singleton.FireConfigurationChanged(this);
            }
        }

        bool UpdatingUI = false;
        private void LoadUIFromConfig(SMX.SMXConfig config)
        {
            // Make sure SaveToConfig doesn't treat these as the user changing values.
            UpdatingUI = true;

            // Set the range for the slider.
            if(config.fsr())
            {
                // 16-bit FSR thresholds.
                slider.Minimum = 5;
                slider.Maximum = 250;
                slider.MinimumDistance = 5;
            } else {
                // 8-bit load cell thresholds
                slider.Minimum = 20;
                slider.Maximum = 200;
                slider.MinimumDistance = 10;
            }

            int lower, upper;
            GetValueFromConfig(config, out lower, out upper);

            // Firmware versions before 4 allowed 0xFF to be used to disable a threshold.
            // This isn't used in newer firmwares.
            if(config.masterVersion < 4 && lower == 0xFF)
            {
                LowerLabel.Content = "Off";
                UpperLabel.Content = "";
            }
            else
            {
                slider.LowerValue = lower;
                slider.UpperValue = upper;
                LowerLabel.Content = lower.ToString();
                UpperLabel.Content = upper.ToString();
            }

            int panelIdx = panelNameToIndex[Type];
            bool ShowThresholdWarning = config.ShowThresholdWarning(panelIdx);
            ThresholdWarning.Visibility = ShowThresholdWarning? Visibility.Visible:Visibility.Hidden;

            RefreshVisibility();
            UpdatingUI = false;
        }

        void RefreshVisibility()
        {
            SMX.SMXConfig config = ActivePad.GetFirstActivePadConfig();
            this.Visibility = ShouldBeDisplayed(config)? Visibility.Visible:Visibility.Collapsed;
        }

        // Return true if this slider should be displayed.  Only display a slider if it affects
        // at least one panel which is enabled.
        private bool ShouldBeDisplayed(SMX.SMXConfig config)
        {
            bool[] enabledPanels = config.GetEnabledPanels();

            // Up and center are shown in both modes.
            switch(Type)
            {
            case "up-left":    return  AdvancedModeEnabled && enabledPanels[0];
            case "up":         return                         enabledPanels[1];
            case "up-right":   return  AdvancedModeEnabled && enabledPanels[2];
            case "left":       return  AdvancedModeEnabled && enabledPanels[3];
            case "center":     return                         enabledPanels[4];
            case "right":      return  AdvancedModeEnabled && enabledPanels[5];
            case "down-left":  return  AdvancedModeEnabled && enabledPanels[6];
            case "down":       return  AdvancedModeEnabled && enabledPanels[7];
            case "down-right": return  AdvancedModeEnabled && enabledPanels[8];

            // Show cardinal and corner if at least one panel they affect is enabled.
            case "cardinal":   return !AdvancedModeEnabled && (enabledPanels[3] || enabledPanels[5] || enabledPanels[8]);
            case "corner":     return !AdvancedModeEnabled && (enabledPanels[0] || enabledPanels[2] || enabledPanels[6] || enabledPanels[8]);
            default:           return true;
            }
        }
    }
    
    // A button with a selectable highlight.
    public class SelectableButton: Button
    {
        public static readonly DependencyProperty SelectedProperty = DependencyProperty.Register("Selected",
            typeof(bool), typeof(SelectableButton), new FrameworkPropertyMetadata(false));
        public bool Selected {
            get { return (bool) GetValue(SelectedProperty); }
            set { SetValue(SelectedProperty, value); }
        }
    }

    // A button that selects a preset, and shows a checkmark if that preset is set.
    public class PresetButton: Control
    {
        public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type",
            typeof(string), typeof(PresetButton), new FrameworkPropertyMetadata(""));
        public string Type {
            get { return (string) GetValue(TypeProperty); }
            set { SetValue(TypeProperty, value); }
        }

        public static readonly DependencyProperty SelectedProperty = DependencyProperty.Register("Selected",
            typeof(bool), typeof(PresetButton), new FrameworkPropertyMetadata(true));
        public bool Selected {
            get { return (bool) GetValue(SelectedProperty); }
            set { SetValue(SelectedProperty, value); }
        }

        public static readonly DependencyProperty LabelProperty = DependencyProperty.Register("Label",
            typeof(string), typeof(PresetButton), new FrameworkPropertyMetadata(""));
        public string Label {
            get { return (string) GetValue(LabelProperty); }
            set { SetValue(LabelProperty, value); }
        }

        private OnConfigChange onConfigChange;

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            Button button = GetTemplateChild("PART_Button") as Button;
            button.Click += delegate(object sender, RoutedEventArgs e) { Select(); };

            onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
                foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
                {
                    SMX.SMXConfig config = activePad.Item2;
                    string CurrentPreset = ConfigPresets.GetPreset(config);
                    Selected = CurrentPreset == Type;
                    break;
                }
            });
        }

        private void Select()
        {
            foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
            {
                int pad = activePad.Item1;
                SMX.SMXConfig config = activePad.Item2;

                ConfigPresets.SetPreset(Type, ref config);
                SMX.SMX.SetConfig(pad, config);
            }
            CurrentSMXDevice.singleton.FireConfigurationChanged(this);
        }
    }

    public class PresetWidget: Control
    {
        public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type",
            typeof(string), typeof(PresetWidget), new FrameworkPropertyMetadata(""));
        public string Type {
            get { return (string) GetValue(TypeProperty); }
            set { SetValue(TypeProperty, value); }
        }

        public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register("Description",
            typeof(string), typeof(PresetWidget), new FrameworkPropertyMetadata(""));
        public string Description {
            get { return (string) GetValue(DescriptionProperty); }
            set { SetValue(DescriptionProperty, value); }
        }

        public static readonly DependencyProperty LabelProperty = DependencyProperty.Register("Label",
            typeof(string), typeof(PresetWidget), new FrameworkPropertyMetadata(""));
        public string Label {
            get { return (string) GetValue(LabelProperty); }
            set { SetValue(LabelProperty, value); }
        }
    }

    public class PanelButton: ToggleButton
    {
        public static readonly DependencyProperty ButtonProperty = DependencyProperty.RegisterAttached("Button",
            typeof(string), typeof(PanelButton), new FrameworkPropertyMetadata(null));

        public string Button {
            get { return (string) this.GetValue(ButtonProperty); }
            set { this.SetValue(ButtonProperty, value); }
        }

        protected override void OnIsPressedChanged(DependencyPropertyChangedEventArgs e)
        {
            base.OnIsPressedChanged(e);
        }
    }

    // A base class for buttons used to select a panel to work with.
    public class PanelSelectButton: Button
    {
        // Whether this button is selected.
        public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.RegisterAttached("IsSelected",
            typeof(bool), typeof(PanelSelectButton), new FrameworkPropertyMetadata(false));

        public bool IsSelected {
            get { return (bool) this.GetValue(IsSelectedProperty); }
            set { this.SetValue(IsSelectedProperty, value); }
        }
    }

    // A button that selects which color is being set.
    public abstract class ColorButton: PanelSelectButton
    {
        // The color configured for this panel:
        public static readonly DependencyProperty PanelColorProperty = DependencyProperty.RegisterAttached("PanelColor",
            typeof(SolidColorBrush), typeof(ColorButton), new FrameworkPropertyMetadata(new SolidColorBrush()));

        public SolidColorBrush PanelColor {
            get { return (SolidColorBrush) this.GetValue(PanelColorProperty); }
            set { this.SetValue(PanelColorProperty, value); }
        }

        // Return 0 if this is for the P1 pad, or 1 if it's for P2.
        protected abstract int getPadNo();

        // Return true if this panel is enabled and should be selectable.
        public abstract bool isEnabled(LoadFromConfigDelegateArgs args);

        // Get and set our color to the pad configuration.
        abstract public Color getColor();
        abstract public void setColor(Color color);

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            OnConfigChange onConfigChange;
            onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
                LoadUIFromConfig(args);
            });
        }

        // Set PanelColor.  This widget doesn't change the color, it only reflects the current configuration.
        private void LoadUIFromConfig(LoadFromConfigDelegateArgs args)
        {
            SMX.SMXConfig config = args.controller[getPadNo()].config;

            // Hide disabled color buttons.
            Visibility = isEnabled(args)? Visibility.Visible:Visibility.Hidden;

            Color rgb = getColor();
            PanelColor = new SolidColorBrush(rgb);
        }

        Point MouseDownPosition;

        protected override void OnMouseDown(MouseButtonEventArgs e)
        {
            MouseDownPosition = e.GetPosition(null);
            base.OnMouseDown(e);
        }

        // Handle initiating drag.
        protected override void OnMouseMove(MouseEventArgs e)
        {
            if(e.LeftButton == MouseButtonState.Pressed)
            {
                Point position = e.GetPosition(null);

                // Why do we have to handle drag thresholding manually?  This is the platform's job.
                // If we don't do this, clicks won't work at all.
                if (Math.Abs(position.X - MouseDownPosition.X) >= SystemParameters.MinimumHorizontalDragDistance ||
                    Math.Abs(position.Y - MouseDownPosition.Y) >= SystemParameters.MinimumVerticalDragDistance)
                {
                    DragDrop.DoDragDrop(this, Helpers.ColorToString(PanelColor.Color), DragDropEffects.Copy);
                }
            }

            base.OnMouseMove(e);
        }

        private bool HandleDrop(DragEventArgs e)
        {
            PanelColorButton Button = e.Source as PanelColorButton;
            if(Button == null)
                return false;

            // A color is being dropped from another button.  Don't just update our color, since
            // that will just change the button color and not actually apply it.
            DataObject data = e.Data as DataObject;
            if(data == null)
                return false;

            // Parse the color being dragged onto us, and set it.
            Color color = Helpers.ParseColorString(data.GetData(typeof(string)) as string);
            setColor(color);

            return true;
        }

        protected override void OnDrop(DragEventArgs e)
        {
            if(!HandleDrop(e))
                base.OnDrop(e);
        }
    }

    // A ColorButton for setting a panel color.
    public class PanelColorButton: ColorButton
    {
        // Which panel this is (P1 0-8, P2 9-17):
        public static readonly DependencyProperty PanelProperty = DependencyProperty.RegisterAttached("Panel",
            typeof(int), typeof(PanelColorButton), new FrameworkPropertyMetadata(0));

        public int Panel {
            get { return (int) this.GetValue(PanelProperty); }
            set { this.SetValue(PanelProperty, value); }
        }
        
        protected override int getPadNo()
        {
            return Panel < 9? 0:1;
        }

        // A panel is enabled if it's enabled in the panel mask, which can be
        // changed on the advanced tab.
        public override bool isEnabled(LoadFromConfigDelegateArgs args)
        {
            int pad = getPadNo();
            SMX.SMXConfig config = args.controller[pad].config;

            if(!args.controller[pad].info.connected)
                return false;

            int PanelIndex = Panel % 9;
            bool[] enabledPanels = config.GetEnabledPanels();
            return enabledPanels[PanelIndex];
        }

        public override void setColor(Color color)
        {
            // Apply the change and save it to the device.
            int pad = getPadNo();
            SMX.SMXConfig config;
            if(!SMX.SMX.GetConfig(pad, out config))
                return;

            // Light colors are 8-bit values, but we only use values between 0-170.  Higher values
            // don't make the panel noticeably brighter, and just draw more power.
            int PanelIndex = Panel % 9;
            config.stepColor[PanelIndex*3+0] = Helpers.ScaleColor(color.R);
            config.stepColor[PanelIndex*3+1] = Helpers.ScaleColor(color.G);
            config.stepColor[PanelIndex*3+2] = Helpers.ScaleColor(color.B);

            SMX.SMX.SetConfig(pad, config);
            CurrentSMXDevice.singleton.FireConfigurationChanged(this);
        }

        // Return the color set for this panel in config.
        public override Color getColor()
        {
            int pad = getPadNo();
            SMX.SMXConfig config;
            if(!SMX.SMX.GetConfig(pad, out config))
                return Color.FromRgb(0,0,0);

            int PanelIndex = Panel % 9;
            return Helpers.UnscaleColor(Color.FromRgb(
                config.stepColor[PanelIndex*3+0],
                config.stepColor[PanelIndex*3+1],
                config.stepColor[PanelIndex*3+2]));
        }
    }

    public class FloorColorButton: ColorButton
    {
        // 0 if this is for P1, 1 for P2.
        public static readonly DependencyProperty PadProperty = DependencyProperty.RegisterAttached("Pad",
            typeof(int), typeof(FloorColorButton), new FrameworkPropertyMetadata(0));

        public int Pad {
            get { return (int) this.GetValue(PadProperty); }
            set { this.SetValue(PadProperty, value); }
        }
        protected override int getPadNo() { return Pad; }

        // The floor color button is available if the firmware is v4 or greater.
        public override bool isEnabled(LoadFromConfigDelegateArgs args)
        {
            int pad = getPadNo();
            SMX.SMXConfig config = args.controller[pad].config;
            return config.masterVersion >= 4;
        }
        
        public override void setColor(Color color)
        {
            // Apply the change and save it to the device.
            int pad = getPadNo();
            SMX.SMXConfig config;
            if(!SMX.SMX.GetConfig(pad, out config))
                    return;

            config.platformStripColor[0] = color.R;
            config.platformStripColor[1] = color.G;
            config.platformStripColor[2] = color.B;

            SMX.SMX.SetConfig(pad, config);
            CurrentSMXDevice.singleton.FireConfigurationChanged(this);
        }

        // Return the color set for this panel in config.
        public override Color getColor()
        {
            int pad = getPadNo();
            SMX.SMXConfig config;
            if(!SMX.SMX.GetConfig(pad, out config))
                return Color.FromRgb(0,0,0);

            return Color.FromRgb(config.platformStripColor[0], config.platformStripColor[1], config.platformStripColor[2]);
        }
    }

    // This is a Slider class with some added helpers.
    public class Slider2: Slider
    {
        public delegate void DragEvent();
        public event DragEvent StartedDragging, StoppedDragging;

        protected Thumb Thumb;

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            Track track = Template.FindName("PART_Track", this) as Track;
            Thumb = track.Thumb;
        }

        // How are there no events for this?
        protected override void OnThumbDragStarted(DragStartedEventArgs e)
        {
            base.OnThumbDragStarted(e);
            StartedDragging?.Invoke();
        }

        protected override void OnThumbDragCompleted(DragCompletedEventArgs e)
        {
            base.OnThumbDragCompleted(e);
            StoppedDragging?.Invoke();
        }

        public Slider2()
        {
            // Fix the slider not dragging after clicking outside the thumb.
            // http://stackoverflow.com/a/30575638/136829
            bool clickedInSlider = false;
            MouseMove += delegate(object sender, MouseEventArgs args)
            {
                if(args.LeftButton == MouseButtonState.Released || !clickedInSlider || Thumb.IsDragging)
                    return;

                Thumb.RaiseEvent(new MouseButtonEventArgs(args.MouseDevice, args.Timestamp, MouseButton.Left)
                {
                    RoutedEvent = UIElement.MouseLeftButtonDownEvent,
                    Source = args.Source,
                });
            };

            AddHandler(UIElement.PreviewMouseLeftButtonDownEvent, new RoutedEventHandler((sender, args) =>
            {
                clickedInSlider = true;
            }), true);

            AddHandler(UIElement.PreviewMouseLeftButtonUpEvent, new RoutedEventHandler((sender, args) =>
            {
                clickedInSlider = false;
            }), true);
        }
    };

    // This is the Slider inside a ColorPicker.
    public class ColorPickerSlider: Slider2
    {
        public ColorPickerSlider()
        {
        }
    };

    public class ColorPicker: Control
    {
        ColorPickerSlider HueSlider;
        public delegate void Event();

        // The selected ColorButton.  This handles getting and setting the color to the
        // config.
        ColorButton _colorButton;
        public ColorButton colorButton {
            get { return _colorButton; }
            set {
                _colorButton = value;

                // Refresh on change.
                LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState();
                LoadUIFromConfig(args);
            }
        }
        

        public event Event StartedDragging, StoppedDragging;
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            HueSlider = GetTemplateChild("HueSlider") as ColorPickerSlider;
            HueSlider.ValueChanged += delegate(object sender, RoutedPropertyChangedEventArgs<double> e) {
                SaveToConfig();
            };

            HueSlider.StartedDragging += delegate() { StartedDragging?.Invoke(); };
            HueSlider.StoppedDragging += delegate() { StoppedDragging?.Invoke(); };

            DoubleCollection ticks = new DoubleCollection();
            // Add a tick at the minimum value, which is a negative value.  This is the
            // tick for white.
            ticks.Add(HueSlider.Minimum);

            // Add a tick for 0-359.  Don't add 360, since that's the same as 0.
            for(int i = 0; i < 360; ++i)
                ticks.Add(i);
            HueSlider.Ticks = ticks;

            OnConfigChange onConfigChange;
            onConfigChange = new OnConfigChange(this, delegate(LoadFromConfigDelegateArgs args) {
                LoadUIFromConfig(args);
            });
        }

        private void SaveToConfig()
        {
            if(UpdatingUI || _colorButton == null)
                return;

            Color color = Helpers.FromHSV(HueSlider.Value, 1, 1);

            // If we're set to the minimum value, use white instead.
            if(HueSlider.Value == HueSlider.Minimum)
                color = Color.FromRgb(255,255,255);

            _colorButton.setColor(color);
        }

        bool UpdatingUI = false;
        private void LoadUIFromConfig(LoadFromConfigDelegateArgs args)
        {
            if(UpdatingUI || _colorButton == null)
                return;

            // Make sure SaveToConfig doesn't treat these as the user changing values.
            UpdatingUI = true;

            // Reverse the scaling we applied in SaveToConfig.
            Color rgb = _colorButton.getColor();
            double h, s, v;
            Helpers.ToHSV(rgb, out h, out s, out v);

            // Check for white.  Since the conversion through LightsScaleFactor may not round trip
            // back to exactly #FFFFFF, give some room for error in the value (brightness).
            if(s <= 0.001 && v >= .90)
            {
                // This is white, so set it to the white block at the left edge of the slider.
                HueSlider.Value = HueSlider.Minimum;
            }
            else
            {
                HueSlider.Value = h;
            }

            UpdatingUI = false;
        }
    };

    // This widget selects which panels are enabled.  We only show one of these for both pads.
    class PanelSelector: Control
    {
        PanelButton[] EnabledPanelButtons;
        OnConfigChange onConfigChange;

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            int[] PanelToIndex = new int[] {
                7, 8, 9,
                4, 5, 6,
                1, 2, 3,
            };

            EnabledPanelButtons = new PanelButton[9];
            for(int i = 0; i < 9; ++i)
                EnabledPanelButtons[i] = GetTemplateChild("EnablePanel" + PanelToIndex[i]) as PanelButton;

            foreach(PanelButton button in EnabledPanelButtons)
                button.Click += EnabledPanelButtonClicked;

            onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
                LoadUIFromConfig(ActivePad.GetFirstActivePadConfig(args));
            });
        }

        private void LoadUIFromConfig(SMX.SMXConfig config)
        {
            // The firmware configuration allows disabling each of the four sensors in a panel
            // individually, but currently we only have a UI for toggling the whole sensor.  Taking
            // individual sensors out isn't recommended.
            bool[] enabledPanels = {
                (config.enabledSensors[0] & 0xF0) != 0,
                (config.enabledSensors[0] & 0x0F) != 0,
                (config.enabledSensors[1] & 0xF0) != 0,
                (config.enabledSensors[1] & 0x0F) != 0,
                (config.enabledSensors[2] & 0xF0) != 0,
                (config.enabledSensors[2] & 0x0F) != 0,
                (config.enabledSensors[3] & 0xF0) != 0,
                (config.enabledSensors[3] & 0x0F) != 0,
                (config.enabledSensors[4] & 0xF0) != 0,
            };
            
            for(int i = 0; i < 9; ++i)
                EnabledPanelButtons[i].IsChecked = enabledPanels[i];
        }

        private int GetIndexFromButton(object sender)
        {
            for(int i = 0; i < 9; i++)
            {
                if(sender == EnabledPanelButtons[i])
                    return i;
            }

            return 0;
        }

        private void EnabledPanelButtonClicked(object sender, EventArgs e)
        {
            // One of the panel buttons on the panel toggle UI was clicked.  Toggle the
            // panel.
            int button = GetIndexFromButton(sender);
            Console.WriteLine("Clicked " + button);

            // Set the enabled sensor mask on both pads to the state of the UI.
            foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
            {
                int pad = activePad.Item1;
                SMX.SMXConfig config = activePad.Item2;

                // This could be done algorithmically, but this is clearer.
                int[] PanelButtonToSensorIndex = {
                    0, 0, 1, 1, 2, 2, 3, 3, 4
                };
                byte[] PanelButtonToSensorMask = {
                    0xF0, 0x0F,
                    0xF0, 0x0F,
                    0xF0, 0x0F,
                    0xF0, 0x0F,
                    0xF0,
                };
                for(int i = 0; i < 5; ++i)
                    config.enabledSensors[i] = 0;

                for(int Panel = 0; Panel < 9; ++Panel)
                {
                    int index = PanelButtonToSensorIndex[Panel];
                    byte mask = PanelButtonToSensorMask[Panel];
                    if(EnabledPanelButtons[Panel].IsChecked == true)
                        config.enabledSensors[index] |= (byte) mask;
                }

                // If we're not in "light all panels" mode, sync up autoLightPanelMask
                // with the new enabledSensors.
                config.refreshAutoLightPanelMask();

                SMX.SMX.SetConfig(pad, config);
            }
        }
    };

    public class FrameImage: Image
    {
        // The source image.  Changing this after load isn't supported.
        public static readonly DependencyProperty ImageProperty = DependencyProperty.Register("Image",
            typeof(BitmapSource), typeof(FrameImage), new FrameworkPropertyMetadata(null, ImageChangedCallback));

        public BitmapSource Image {
            get { return (BitmapSource) this.GetValue(ImageProperty); }
            set { this.SetValue(ImageProperty, value); }
        }

        // Which frame is currently displayed:
        public static readonly DependencyProperty FrameProperty = DependencyProperty.Register("Frame",
            typeof(int), typeof(FrameImage), new FrameworkPropertyMetadata(0, FrameChangedCallback));

        public int Frame {
            get { return (int) this.GetValue(FrameProperty); }
            set { this.SetValue(FrameProperty, value); }
        }

        public static readonly DependencyProperty FramesXProperty = DependencyProperty.Register("FramesX",
            typeof(int), typeof(FrameImage), new FrameworkPropertyMetadata(0, ImageChangedCallback));

        public int FramesX {
            get { return (int) this.GetValue(FramesXProperty); }
            set { this.SetValue(FramesXProperty, value); }
        }

        private static void ImageChangedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args)
        {
            FrameImage self = target as FrameImage;
            self.Load();
        }

        private static void FrameChangedCallback(DependencyObject target, DependencyPropertyChangedEventArgs args)
        {
            FrameImage self = target as FrameImage;
            self.Refresh();
        }

        private BitmapSource[] ImageFrames;

        private void Load()
        {
            if(Image == null || FramesX == 0)
            {
                ImageFrames = null;
                return;
            }

            // Split the image into frames.
            int FrameWidth = Image.PixelWidth / FramesX;
            int FrameHeight = Image.PixelHeight;
            ImageFrames = new BitmapSource[FramesX];
            for(int i = 0; i < FramesX; ++i)
                ImageFrames[i] = new CroppedBitmap(Image, new Int32Rect(FrameWidth*i, 0, FrameWidth, FrameHeight));

            Refresh();
        }

        private void Refresh()
        {
            if(ImageFrames == null || Frame >= ImageFrames.Length)
            {
                this.Source = null;
                return;
            }

            this.Source = ImageFrames[Frame];
        }
    };

    public class LightAllPanelsCheckbox: CheckBox
    {
        public static readonly DependencyProperty LightAllPanelsProperty = DependencyProperty.Register("LightAllPanels",
            typeof(bool), typeof(LightAllPanelsCheckbox), new FrameworkPropertyMetadata(false));
        public bool LightAllPanels {
            get { return (bool) GetValue(LightAllPanelsProperty); }
            set { SetValue(LightAllPanelsProperty, value); }
        }

        OnConfigChange onConfigChange;

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
                LoadUIFromConfig(ActivePad.GetFirstActivePadConfig(args));
            });
        }

        private void LoadUIFromConfig(SMX.SMXConfig config)
        {
            LightAllPanels = config.getLightAllPanelsMode();
        }

        protected override void OnClick()
        {
            //SMX.SMXConfig firstConfig = ActivePad.GetFirstActivePadConfig();
            foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
            {
                int pad = activePad.Item1;
                SMX.SMXConfig config = activePad.Item2;
                config.setLightAllPanelsMode(!LightAllPanels);
                SMX.SMX.SetConfig(pad, config);
            }
            CurrentSMXDevice.singleton.FireConfigurationChanged(this);

            // Refresh the UI.
            //LoadUIFromConfig(firstConfig);
        }
    }

    public class EnableCenterTopSensorCheckbox: CheckBox
    {
        public static readonly DependencyProperty EnableSensorProperty = DependencyProperty.Register("EnableSensor",
            typeof(bool), typeof(EnableCenterTopSensorCheckbox), new FrameworkPropertyMetadata(false));
        public bool EnableSensor {
            get { return (bool) GetValue(EnableSensorProperty); }
            set { SetValue(EnableSensorProperty, value); }
        }

        OnConfigChange onConfigChange;

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            onConfigChange = new OnConfigChange(this, delegate (LoadFromConfigDelegateArgs args) {
                LoadUIFromConfig(ActivePad.GetFirstActivePadConfig(args));
            });
        }

        private void LoadUIFromConfig(SMX.SMXConfig config)
        {
            // Center panel, top sensor:
            bool enabled = config.panelSettings[4].fsrHighThreshold[2] < 255;
            EnableSensor = enabled;
        }

        protected override void OnClick()
        {
            foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
            {
                int pad = activePad.Item1;
                SMX.SMXConfig config = activePad.Item2;

                // Disable the sensor by setting its high threshold to 255, and enable it by syncing it up
                // with the other thresholds.
                if(!EnableSensor)
                    config.panelSettings[4].fsrHighThreshold[2] = config.panelSettings[4].fsrHighThreshold[0];
                else
                    config.panelSettings[4].fsrHighThreshold[2] = 255;
                SMX.SMX.SetConfig(pad, config);
            }
            CurrentSMXDevice.singleton.FireConfigurationChanged(this);
        }
    }
}