/* Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution.
 */

public class RGBHistogramManipulator : Gtk.DrawingArea {
    private enum LocationCode { LEFT_NUB, RIGHT_NUB, LEFT_TROUGH, RIGHT_TROUGH,
        INSENSITIVE_AREA }
    private const int NUB_SIZE = 13;
    private const int NUB_HALF_WIDTH = NUB_SIZE / 2;
    private const int NUB_V_NUDGE = 4;
    private const int TROUGH_WIDTH = 256 + (2 * NUB_HALF_WIDTH);
    private const int TROUGH_HEIGHT = 4;
    private const int TROUGH_BOTTOM_OFFSET = 1;
    private const int CONTROL_WIDTH = TROUGH_WIDTH + 2;
    private const int CONTROL_HEIGHT = 118;
    private const int NUB_V_POSITION = CONTROL_HEIGHT - TROUGH_HEIGHT - TROUGH_BOTTOM_OFFSET
        - (NUB_SIZE - TROUGH_HEIGHT) / 2 - NUB_V_NUDGE - 2;
    private int left_nub_max = 255 - NUB_SIZE - 1;
    private int right_nub_min = NUB_SIZE + 1;

    private static Gtk.WidgetPath slider_draw_path = new Gtk.WidgetPath();
    private static Gtk.WidgetPath frame_draw_path = new Gtk.WidgetPath();
    private static bool paths_setup = false;

    private RGBHistogram histogram = null;
    private int left_nub_position = 0;
    private int right_nub_position = 255;
    private bool is_left_nub_tracking = false;
    private bool is_right_nub_tracking = false;
    private int track_start_x = 0;
    private int track_nub_start_position = 0;
    private int offset = 0;

    public RGBHistogramManipulator( ) {
        set_size_request(CONTROL_WIDTH, CONTROL_HEIGHT);
        can_focus = true;
        
        if (!paths_setup) {
            slider_draw_path.append_type(typeof(Gtk.Scale));
            slider_draw_path.iter_add_class(0, "scale");
            slider_draw_path.iter_add_class(0, "range");
            
            frame_draw_path.append_type(typeof(Gtk.Frame));
            frame_draw_path.iter_add_class(0, "default");
            
            paths_setup = true;
        }
            
        add_events(Gdk.EventMask.BUTTON_PRESS_MASK);
        add_events(Gdk.EventMask.BUTTON_RELEASE_MASK);
        add_events(Gdk.EventMask.BUTTON_MOTION_MASK);
        add_events(Gdk.EventMask.FOCUS_CHANGE_MASK);
        add_events(Gdk.EventMask.KEY_PRESS_MASK);

        button_press_event.connect(on_button_press);
        button_release_event.connect(on_button_release);
        motion_notify_event.connect(on_button_motion);

        this.size_allocate.connect(on_size_allocate);
    }

    private void on_size_allocate(Gtk.Allocation region) {
        this.offset = (region.width - RGBHistogram.GRAPHIC_WIDTH - NUB_SIZE) / 2;
    }

    private LocationCode hit_test_point(int x, int y) {
        if (y < NUB_V_POSITION)
            return LocationCode.INSENSITIVE_AREA;

        if ((x > left_nub_position) && (x < left_nub_position + NUB_SIZE))
            return LocationCode.LEFT_NUB;

        if ((x > right_nub_position) && (x < right_nub_position + NUB_SIZE))
            return LocationCode.RIGHT_NUB;

        if (y < (NUB_V_POSITION + NUB_V_NUDGE + 1))
            return LocationCode.INSENSITIVE_AREA;

        if ((x - left_nub_position) * (x - left_nub_position) <
            (x - right_nub_position) * (x - right_nub_position))
            return LocationCode.LEFT_TROUGH;
        else
            return LocationCode.RIGHT_TROUGH;
    }
    
    private bool on_button_press(Gdk.EventButton event_record) {
        // Adjust mouse position to drawing offset
        // Easier to modify the event and shit the whole drawing then adjusting the nub drawing code
        event_record.x -= this.offset;
        LocationCode loc = hit_test_point((int) event_record.x, (int) event_record.y);
        bool retval = true;

        switch (loc) {
            case LocationCode.LEFT_NUB:
                track_start_x = ((int) event_record.x);
                track_nub_start_position = left_nub_position;
                is_left_nub_tracking = true;
                break;

            case LocationCode.RIGHT_NUB:
                track_start_x = ((int) event_record.x);
                track_nub_start_position = right_nub_position;
                is_right_nub_tracking = true;
                break;

            case LocationCode.LEFT_TROUGH:
                left_nub_position = ((int) event_record.x) - NUB_HALF_WIDTH;
                left_nub_position = left_nub_position.clamp(0, left_nub_max);
                force_update();
                nub_position_changed();
                update_nub_extrema();
                break;

            case LocationCode.RIGHT_TROUGH:
                right_nub_position = ((int) event_record.x) - NUB_HALF_WIDTH;
                right_nub_position = right_nub_position.clamp(right_nub_min, 255);
                force_update();
                nub_position_changed();
                update_nub_extrema();
                break;

            default:
                retval = false;
                break;
        }

        // Remove adjustment position to drawing offset
        event_record.x += this.offset;

        return retval;
    }
    
    private bool on_button_release(Gdk.EventButton event_record) {
        if (is_left_nub_tracking || is_right_nub_tracking) {
            nub_position_changed();
            update_nub_extrema();
        }

        is_left_nub_tracking = false;
        is_right_nub_tracking = false;

        return false;
    }
    
    private bool on_button_motion(Gdk.EventMotion event_record) {
        if ((!is_left_nub_tracking) && (!is_right_nub_tracking))
            return false;
    
        event_record.x -= this.offset;
        if (is_left_nub_tracking) {
            int track_x_delta = ((int) event_record.x) - track_start_x;
            left_nub_position = (track_nub_start_position + track_x_delta);
            left_nub_position = left_nub_position.clamp(0, left_nub_max);
        } else { /* right nub is tracking */
            int track_x_delta = ((int) event_record.x) - track_start_x;
            right_nub_position = (track_nub_start_position + track_x_delta);
            right_nub_position = right_nub_position.clamp(right_nub_min, 255);
        }
        
        force_update();
        event_record.x += this.offset;

        return true;
    }

    public override bool focus_out_event(Gdk.EventFocus event) {
        if (base.focus_out_event(event)) {
            return true;
        }

        queue_draw();

        return false;
    }

    public override bool key_press_event(Gdk.EventKey event) {
        if (base.key_press_event(event)) {
            return true;
        }

        int delta = 0;

        if (event.keyval == Gdk.Key.Left || event.keyval == Gdk.Key.Up) {
            delta = -1;
        }

        if (event.keyval == Gdk.Key.Right || event.keyval == Gdk.Key.Down) {
            delta = 1;
        }

        if (!(Gdk.ModifierType.CONTROL_MASK in event.state)) {
            delta *= 5;
        }

        if (delta == 0) {
            return false;
        }

        if (Gdk.ModifierType.SHIFT_MASK in event.state) {
            right_nub_position += delta;
            right_nub_position = right_nub_position.clamp(right_nub_min, 255);
        } else {
            left_nub_position += delta;
            left_nub_position = left_nub_position.clamp(0, left_nub_max);

        }

        nub_position_changed();
        update_nub_extrema();
        force_update();

        return true;
    }
    
    public override bool draw(Cairo.Context ctx) {
        Gtk.Border padding = get_style_context().get_padding(Gtk.StateFlags.NORMAL);

        Gdk.Rectangle area = Gdk.Rectangle();
        area.x = padding.left + this.offset;
        area.y = padding.top;
        area.width = RGBHistogram.GRAPHIC_WIDTH + padding.right;
        area.height = RGBHistogram.GRAPHIC_HEIGHT + padding.bottom;

        if (has_focus) {
            get_style_context().render_focus(ctx, area.x, area.y,
                                             area.width + NUB_SIZE,
                                             area.height + NUB_SIZE + NUB_HALF_WIDTH);
        }

        draw_histogram(ctx, area);
        draw_nub(ctx, area, left_nub_position);
        draw_nub(ctx, area, right_nub_position);

        return true;
    }
    
    private void draw_histogram(Cairo.Context ctx, Gdk.Rectangle area) {
        if (histogram == null)
            return;

        var histogram_graphic = histogram.get_graphic();

        Gdk.cairo_set_source_pixbuf(ctx, histogram_graphic, area.x + NUB_HALF_WIDTH, area.y + 2);
        ctx.paint();

        if (left_nub_position > 0) {
            ctx.rectangle(area.x + NUB_HALF_WIDTH, area.y + 2,
                          left_nub_position,
                          histogram_graphic.height);
            ctx.set_source_rgba(0.0, 0.0, 0.0, 0.45);
            ctx.fill();
        }

        if (right_nub_position < 255) {
            ctx.rectangle(area.x + right_nub_position + NUB_HALF_WIDTH,
                          area.y + 2,
                          histogram_graphic.width - right_nub_position,
                          histogram_graphic.height);
            ctx.set_source_rgba(1.0, 1.0, 1.0, 0.45);
            ctx.fill();
        }
    }

    private void draw_nub(Cairo.Context ctx, Gdk.Rectangle area, int position) {
        ctx.move_to(area.x + position, area.y + NUB_V_POSITION + NUB_SIZE);
        ctx.line_to(area.x + position + NUB_HALF_WIDTH, area.y + NUB_V_POSITION);
        ctx.line_to(area.x + position + NUB_SIZE, area.y + NUB_V_POSITION + NUB_SIZE);
        ctx.close_path();
        ctx.set_source_rgb(0.333, 0.333, 0.333);
        ctx.fill();
    }
    
    private void force_update() {
        get_window().invalidate_rect(null, true);
    }
    
    private void update_nub_extrema() {
        right_nub_min = left_nub_position + NUB_SIZE + 1;
        left_nub_max = right_nub_position - NUB_SIZE - 1;
    }

    public signal void nub_position_changed();

    public void update_histogram(Gdk.Pixbuf source_pixbuf) {
        histogram = new RGBHistogram(source_pixbuf);
        force_update();
    }
    
    public int get_left_nub_position() {
        return left_nub_position;
    }
    
    public int get_right_nub_position() {
        return right_nub_position;
    }

    public void set_left_nub_position(int user_nub_pos) {
        assert ((user_nub_pos >= 0) && (user_nub_pos <= 255));
        left_nub_position = user_nub_pos.clamp(0, left_nub_max);
        update_nub_extrema();
    }
    
    public void set_right_nub_position(int user_nub_pos) {
        assert ((user_nub_pos >= 0) && (user_nub_pos <= 255));
        right_nub_position = user_nub_pos.clamp(right_nub_min, 255);
        update_nub_extrema();
    }
}