/*
 * Copyright (C) 2009-2015 Canonical Ltd.
 * Author: Robert Ancell <robert.ancell@canonical.com>
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
 * license.
 */

public enum CropLocation
{
    NONE = 0,
    MIDDLE,
    TOP,
    BOTTOM,
    LEFT,
    RIGHT,
    TOP_LEFT,
    TOP_RIGHT,
    BOTTOM_LEFT,
    BOTTOM_RIGHT
}

public class PageView : Object
{
    /* Page being rendered */
    public Page page { get; private set; }

    /* Image to render at current resolution */
    private Gdk.Pixbuf? image = null;

    /* Border around image */
    private bool selected_ = false;
    public bool selected
    {
        get { return selected_; }
        set
        {
            if ((this.selected && selected) || (!this.selected && !selected))
                return;
            this.selected = selected;
            changed ();
        }
    }

    private int ruler_width = 8;

    private int border_width = 2;

    /* True if image needs to be regenerated */
    private bool update_image = true;

    /* Direction of currently scanned image */
    private ScanDirection scan_direction;

    /* Next scan line to render */
    private int scan_line;

    /* Dimensions of image to generate */
    private int width_;
    private int height_;

    /* Location to place this page */
    public int x_offset { get; set; }
    public int y_offset { get; set; }

    private CropLocation crop_location;
    private double selected_crop_px;
    private double selected_crop_py;
    private int selected_crop_x;
    private int selected_crop_y;
    private int selected_crop_w;
    private int selected_crop_h;

    /* Cursor over this page */
    public Gdk.CursorType cursor { get; private set; default = Gdk.CursorType.ARROW; }

    private int animate_n_segments = 7;
    private int animate_segment;
    private uint animate_timeout;

    public signal void size_changed ();
    public signal void changed ();

    public PageView (Page page)
    {
        this.page = page;
        page.pixels_changed.connect (page_pixels_changed_cb);
        page.size_changed.connect (page_size_changed_cb);
        page.crop_changed.connect (page_overlay_changed_cb);
        page.scan_line_changed.connect (page_overlay_changed_cb);
        page.scan_direction_changed.connect (scan_direction_changed_cb);
    }

    ~PageView ()
    {
        page.pixels_changed.disconnect (page_pixels_changed_cb);
        page.size_changed.disconnect (page_size_changed_cb);
        page.crop_changed.disconnect (page_overlay_changed_cb);
        page.scan_line_changed.disconnect (page_overlay_changed_cb);
        page.scan_direction_changed.disconnect (scan_direction_changed_cb);
    }

    private uchar get_sample (uchar[] pixels, int offset, int x, int depth, int sample)
    {
        // FIXME
        return 0xFF;
    }

    private void get_pixel (Page page, int x, int y, uchar[] pixel)
    {
        switch (page.scan_direction)
        {
        case ScanDirection.TOP_TO_BOTTOM:
            break;
        case ScanDirection.BOTTOM_TO_TOP:
            x = page.scan_width - x - 1;
            y = page.scan_height - y - 1;
            break;
        case ScanDirection.LEFT_TO_RIGHT:
            var t = x;
            x = page.scan_width - y - 1;
            y = t;
            break;
        case ScanDirection.RIGHT_TO_LEFT:
            var t = x;
            x = y;
            y = page.scan_height - t - 1;
            break;
        }

        var depth = page.depth;
        var n_channels = page.n_channels;
        unowned uchar[] pixels = page.get_pixels ();
        var offset = page.rowstride * y;

        /* Optimise for 8 bit images */
        if (depth == 8 && n_channels == 3)
        {
            var o = offset + x * n_channels;
            pixel[0] = pixels[o];
            pixel[1] = pixels[o+1];
            pixel[2] = pixels[o+2];
            return;
        }
        else if (depth == 8 && n_channels == 1)
        {
            pixel[0] = pixel[1] = pixel[2] = pixels[offset + x];
            return;
        }

        /* Optimise for bitmaps */
        else if (depth == 1 && n_channels == 1)
        {
            var o = offset + (x / 8);
            pixel[0] = pixel[1] = pixel[2] = (pixels[o] & (0x80 >> (x % 8))) != 0 ? 0x00 : 0xFF;
            return;
        }

        /* Optimise for 2 bit images */
        else if (depth == 2 && n_channels == 1)
        {
            int block_shift[4] = { 6, 4, 2, 0 };

            var o = offset + (x / 4);
            var sample = (pixels[o] >> block_shift[x % 4]) & 0x3;
            sample = sample * 255 / 3;

            pixel[0] = pixel[1] = pixel[2] = (uchar) sample;
            return;
        }

        /* Use slow method */
        pixel[0] = get_sample (pixels, offset, x, depth, x * n_channels);
        pixel[1] = get_sample (pixels, offset, x, depth, x * n_channels + 1);
        pixel[2] = get_sample (pixels, offset, x, depth, x * n_channels + 2);
    }

    private void set_pixel (Page page, double l, double r, double t, double b, uchar[] output, int offset)
    {
        /* Decimation:
         *
         * Target pixel is defined by (t,l)-(b,r)
         * It touches 16 pixels in original image
         * It completely covers 4 pixels in original image (T,L)-(B,R)
         * Add covered pixels and add weighted partially covered pixels.
         * Divide by total area.
         *
         *      l  L           R   r
         *   +-----+-----+-----+-----+
         *   |     |     |     |     |
         * t |  +--+-----+-----+---+ |
         * T +--+--+-----+-----+---+-+
         *   |  |  |     |     |   | |
         *   |  |  |     |     |   | |
         *   +--+--+-----+-----+---+-+
         *   |  |  |     |     |   | |
         *   |  |  |     |     |   | |
         * B +--+--+-----+-----+---+-+
         *   |  |  |     |     |   | |
         * b |  +--+-----+-----+---+ |
         *   +-----+-----+-----+-----+
         *
         *
         * Interpolation:
         *
         *             l    r
         *   +-----+-----+-----+-----+
         *   |     |     |     |     |
         *   |     |     |     |     |
         *   +-----+-----+-----+-----+
         * t |     |   +-+--+  |     |
         *   |     |   | |  |  |     |
         *   +-----+---+-+--+--+-----+
         * b |     |   +-+--+  |     |
         *   |     |     |     |     |
         *   +-----+-----+-----+-----+
         *   |     |     |     |     |
         *   |     |     |     |     |
         *   +-----+-----+-----+-----+
         *
         * Same again, just no completely covered pixels.
         */

        var L = (int) l;
        if (L != l)
            L++;
        var R = (int) r;
        var T = (int) t;
        if (T != t)
            T++;
        var B = (int) b;

        var red = 0.0;
        var green = 0.0;
        var blue = 0.0;

        /* Target can fit inside one source pixel
         * +-----+
         * |     |
         * | +--+|      +-----+-----+      +-----+      +-----+      +-----+
         * +-+--++  or  |   +-++    |  or  | +-+ |  or  | +--+|  or  |  +--+
         * | +--+|      |   +-++    |      | +-+ |      | |  ||      |  |  |
         * |     |      +-----+-----+      +-----+      +-+--++      +--+--+
         * +-----+
         */
        if ((r - l <= 1.0 && (int)r == (int)l) || (b - t <= 1.0 && (int)b == (int)t))
        {
            /* Inside */
            if ((int)l == (int)r || (int)t == (int)b)
            {
                uchar p[3];
                get_pixel (page, (int)l, (int)t, p);
                output[offset] = p[0];
                output[offset+1] = p[1];
                output[offset+2] = p[2];
                return;
            }

            /* Stradling horizontal edge */
            if (L > R)
            {
                uchar p[3];
                get_pixel (page, R, T-1, p);
                red   += p[0] * (r-l)*(T-t);
                green += p[1] * (r-l)*(T-t);
                blue  += p[2] * (r-l)*(T-t);
                for (var y = T; y < B; y++)
                {
                    get_pixel (page, R, y, p);
                    red   += p[0] * (r-l);
                    green += p[1] * (r-l);
                    blue  += p[2] * (r-l);
                }
                get_pixel (page, R, B, p);
                red   += p[0] * (r-l)*(b-B);
                green += p[1] * (r-l)*(b-B);
                blue  += p[2] * (r-l)*(b-B);
            }
            /* Stradling vertical edge */
            else
            {
                uchar p[3];
                get_pixel (page, L - 1, B, p);
                red   += p[0] * (b-t)*(L-l);
                green += p[1] * (b-t)*(L-l);
                blue  += p[2] * (b-t)*(L-l);
                for (var x = L; x < R; x++) {
                    get_pixel (page, x, B, p);
                    red   += p[0] * (b-t);
                    green += p[1] * (b-t);
                    blue  += p[2] * (b-t);
                }
                get_pixel (page, R, B, p);
                red   += p[0] * (b-t)*(r-R);
                green += p[1] * (b-t)*(r-R);
                blue  += p[2] * (b-t)*(r-R);
            }

            var scale = 1.0 / ((r - l) * (b - t));
            output[offset] = (uchar)(red * scale + 0.5);
            output[offset+1] = (uchar)(green * scale + 0.5);
            output[offset+2] = (uchar)(blue * scale + 0.5);
            return;
        }

        /* Add the middle pixels */
        for (var x = L; x < R; x++)
        {
            for (var y = T; y < B; y++)
            {
                uchar p[3];
                get_pixel (page, x, y, p);
                red   += p[0];
                green += p[1];
                blue  += p[2];
            }
        }

        /* Add the weighted top and bottom pixels */
        for (var x = L; x < R; x++)
        {
            if (t != T)
            {
                uchar p[3];
                get_pixel (page, x, T - 1, p);
                red   += p[0] * (T - t);
                green += p[1] * (T - t);
                blue  += p[2] * (T - t);
            }

            if (b != B)
            {
                uchar p[3];
                get_pixel (page, x, B, p);
                red   += p[0] * (b - B);
                green += p[1] * (b - B);
                blue  += p[2] * (b - B);
            }
        }

        /* Add the left and right pixels */
        for (var y = T; y < B; y++)
        {
            if (l != L)
            {
                uchar p[3];
                get_pixel (page, L - 1, y, p);
                red   += p[0] * (L - l);
                green += p[1] * (L - l);
                blue  += p[2] * (L - l);
            }

            if (r != R)
            {
                uchar p[3];
                get_pixel (page, R, y, p);
                red   += p[0] * (r - R);
                green += p[1] * (r - R);
                blue  += p[2] * (r - R);
            }
        }

        /* Add the corner pixels */
        if (l != L && t != T)
        {
            uchar p[3];
            get_pixel (page, L - 1, T - 1, p);
            red   += p[0] * (L - l)*(T - t);
            green += p[1] * (L - l)*(T - t);
            blue  += p[2] * (L - l)*(T - t);
        }
        if (r != R && t != T)
        {
            uchar p[3];
            get_pixel (page, R, T - 1, p);
            red   += p[0] * (r - R)*(T - t);
            green += p[1] * (r - R)*(T - t);
            blue  += p[2] * (r - R)*(T - t);
        }
        if (r != R && b != B)
        {
            uchar p[3];
            get_pixel (page, R, B, p);
            red   += p[0] * (r - R)*(b - B);
            green += p[1] * (r - R)*(b - B);
            blue  += p[2] * (r - R)*(b - B);
        }
        if (l != L && b != B)
        {
            uchar p[3];
            get_pixel (page, L - 1, B, p);
            red   += p[0] * (L - l)*(b - B);
            green += p[1] * (L - l)*(b - B);
            blue  += p[2] * (L - l)*(b - B);
        }

        /* Scale pixel values and clamp in range [0, 255] */
        var scale = 1.0 / ((r - l) * (b - t));
        output[offset] = (uchar)(red * scale + 0.5);
        output[offset+1] = (uchar)(green * scale + 0.5);
        output[offset+2] = (uchar)(blue * scale + 0.5);
    }

    private void update_preview (Page page, ref Gdk.Pixbuf? output_image, int output_width, int output_height,
                                 ScanDirection scan_direction, int old_scan_line, int scan_line)
    {
        var input_width = page.width;
        var input_height = page.height;

        /* Create new image if one does not exist or has changed size */
        int L, R, T, B;
        if (output_image == null ||
            output_image.width != output_width ||
            output_image.height != output_height)
        {
            output_image = new Gdk.Pixbuf (Gdk.Colorspace.RGB,
                                           false,
                                           8,
                                           output_width,
                                           output_height);

            /* Update entire image */
            L = 0;
            R = output_width - 1;
            T = 0;
            B = output_height - 1;
        }
        /* Otherwise only update changed area */
        else
        {
            switch (scan_direction)
            {
            case ScanDirection.TOP_TO_BOTTOM:
                L = 0;
                R = output_width - 1;
                T = (int)((double)old_scan_line * output_height / input_height);
                B = (int)((double)scan_line * output_height / input_height + 0.5);
                break;
            case ScanDirection.LEFT_TO_RIGHT:
                L = (int)((double)old_scan_line * output_width / input_width);
                R = (int)((double)scan_line * output_width / input_width + 0.5);
                T = 0;
                B = output_height - 1;
                break;
            case ScanDirection.BOTTOM_TO_TOP:
                L = 0;
                R = output_width - 1;
                T = (int)((double)(input_height - scan_line) * output_height / input_height);
                B = (int)((double)(input_height - old_scan_line) * output_height / input_height + 0.5);
                break;
            case ScanDirection.RIGHT_TO_LEFT:
                L = (int)((double)(input_width - scan_line) * output_width / input_width);
                R = (int)((double)(input_width - old_scan_line) * output_width / input_width + 0.5);
                T = 0;
                B = output_height - 1;
                break;
            default:
                L = R = B = T = 0;
                break;
            }
        }

        /* FIXME: There's an off by one error in there somewhere... */
        if (R >= output_width)
            R = output_width - 1;
        if (B >= output_height)
            B = output_height - 1;

        return_if_fail (L >= 0);
        return_if_fail (R < output_width);
        return_if_fail (T >= 0);
        return_if_fail (B < output_height);
        return_if_fail (output_image != null);

        unowned uchar[] output = output_image.get_pixels ();
        var output_rowstride = output_image.rowstride;
        var output_n_channels = output_image.n_channels;

        if (!page.has_data)
        {
            for (var x = L; x <= R; x++)
                for (var y = T; y <= B; y++)
                {
                    var o = output_rowstride * y + x * output_n_channels;
                    output[o] = output[o+1] = output[o+2] = 0xFF;
                }
            return;
        }

        /* Update changed area */
        for (var x = L; x <= R; x++)
        {
            var l = (double)x * input_width / output_width;
            var r = (double)(x + 1) * input_width / output_width;

            for (var y = T; y <= B; y++)
            {
                var t = (double)y * input_height / output_height;
                var b = (double)(y + 1) * input_height / output_height;

                set_pixel (page,
                           l, r, t, b,
                           output, output_rowstride * y + x * output_n_channels);
            }
        }
    }

    private int get_preview_width ()
    {
        return width_ - (border_width + ruler_width) * 2;
    }

    private int get_preview_height ()
    {
        return height_ - (border_width + ruler_width) * 2;
    }

    private void update_page_view ()
    {
        if (!update_image)
            return;

        var old_scan_line = scan_line;
        var scan_line = page.scan_line;

        /* Delete old image if scan direction changed */
        var left_steps = scan_direction - page.scan_direction;
        if (left_steps != 0 && image != null)
            image = null;
        scan_direction = page.scan_direction;

        update_preview (page,
                        ref image,
                        get_preview_width (),
                        get_preview_height (),
                        page.scan_direction, old_scan_line, scan_line);

        update_image = false;
        this.scan_line = scan_line;
    }

    private int page_to_screen_x (int x)
    {
        return (int) ((double)x * get_preview_width () / page.width + 0.5);
    }

    private int page_to_screen_y (int y)
    {
        return (int) ((double)y * get_preview_height () / page.height + 0.5);
    }

    private int screen_to_page_x (int x)
    {
        return (int) ((double)x * page.width / get_preview_width () + 0.5);
    }

    private int screen_to_page_y (int y)
    {
        return (int) ((double)y * page.height / get_preview_height () + 0.5);
    }

    private CropLocation get_crop_location (int x, int y)
    {
        if (!page.has_crop)
            return CropLocation.NONE;

        var cx = page.crop_x;
        var cy = page.crop_y;
        var cw = page.crop_width;
        var ch = page.crop_height;
        var dx = page_to_screen_x (cx) + border_width + ruler_width;
        var dy = page_to_screen_y (cy) + border_width + ruler_width;
        var dw = page_to_screen_x (cw) + border_width + ruler_width;
        var dh = page_to_screen_y (ch) + border_width + ruler_width;
        var ix = x - dx;
        var iy = y - dy;

        if (ix < 0 || ix > dw || iy < 0 || iy > dh)
            return CropLocation.NONE;

        /* Can't resize named crops */
        var name = page.crop_name;
        if (name != null)
            return CropLocation.MIDDLE;

        /* Adjust borders so can select */
        int crop_border = 20;
        if (dw < crop_border * 3)
            crop_border = dw / 3;
        if (dh < crop_border * 3)
            crop_border = dh / 3;

        /* Top left */
        if (ix < crop_border && iy < crop_border)
            return CropLocation.TOP_LEFT;
        /* Top right */
        if (ix > dw - crop_border && iy < crop_border)
            return CropLocation.TOP_RIGHT;
        /* Bottom left */
        if (ix < crop_border && iy > dh - crop_border)
            return CropLocation.BOTTOM_LEFT;
        /* Bottom right */
        if (ix > dw - crop_border && iy > dh - crop_border)
            return CropLocation.BOTTOM_RIGHT;

        /* Left */
        if (ix < crop_border)
            return CropLocation.LEFT;
        /* Right */
        if (ix > dw - crop_border)
            return CropLocation.RIGHT;
        /* Top */
        if (iy < crop_border)
            return CropLocation.TOP;
        /* Bottom */
        if (iy > dh - crop_border)
            return CropLocation.BOTTOM;

        /* In the middle */
        return CropLocation.MIDDLE;
    }

    public void button_press (int x, int y)
    {
        /* See if selecting crop */
        var location = get_crop_location (x, y);
        if (location != CropLocation.NONE)
        {
            crop_location = location;
            selected_crop_px = x;
            selected_crop_py = y;
            selected_crop_x = page.crop_x;
            selected_crop_y = page.crop_y;
            selected_crop_w = page.crop_width;
            selected_crop_h = page.crop_height;
        }
    }

    public void motion (int x, int y)
    {
        var location = get_crop_location (x, y);
        Gdk.CursorType cursor;
        switch (location)
        {
        case CropLocation.MIDDLE:
            cursor = Gdk.CursorType.HAND1;
            break;
        case CropLocation.TOP:
            cursor = Gdk.CursorType.TOP_SIDE;
            break;
        case CropLocation.BOTTOM:
            cursor = Gdk.CursorType.BOTTOM_SIDE;
            break;
        case CropLocation.LEFT:
            cursor = Gdk.CursorType.LEFT_SIDE;
            break;
        case CropLocation.RIGHT:
            cursor = Gdk.CursorType.RIGHT_SIDE;
            break;
        case CropLocation.TOP_LEFT:
            cursor = Gdk.CursorType.TOP_LEFT_CORNER;
            break;
        case CropLocation.TOP_RIGHT:
            cursor = Gdk.CursorType.TOP_RIGHT_CORNER;
            break;
        case CropLocation.BOTTOM_LEFT:
            cursor = Gdk.CursorType.BOTTOM_LEFT_CORNER;
            break;
        case CropLocation.BOTTOM_RIGHT:
            cursor = Gdk.CursorType.BOTTOM_RIGHT_CORNER;
            break;
        default:
            cursor = Gdk.CursorType.ARROW;
            break;
        }

        if (crop_location == CropLocation.NONE)
        {
            this.cursor = cursor;
            return;
        }

        /* Move the crop */
        var pw = page.width;
        var ph = page.height;
        var cw = page.crop_width;
        var ch = page.crop_height;

        var dx = screen_to_page_x (x - (int) selected_crop_px);
        var dy = screen_to_page_y (y - (int) selected_crop_py);

        var new_x = selected_crop_x;
        var new_y = selected_crop_y;
        var new_w = selected_crop_w;
        var new_h = selected_crop_h;

        /* Limit motion to remain within page and minimum crop size */
        var min_size = screen_to_page_x (15);
        if (crop_location == CropLocation.TOP_LEFT ||
            crop_location == CropLocation.LEFT ||
            crop_location == CropLocation.BOTTOM_LEFT)
        {
            if (dx > new_w - min_size)
                dx = new_w - min_size;
            if (new_x + dx < 0)
                dx = -new_x;
        }
        if (crop_location == CropLocation.TOP_LEFT ||
            crop_location == CropLocation.TOP ||
            crop_location == CropLocation.TOP_RIGHT)
        {
            if (dy > new_h - min_size)
                dy = new_h - min_size;
            if (new_y + dy < 0)
                dy = -new_y;
        }

        if (crop_location == CropLocation.TOP_RIGHT ||
            crop_location == CropLocation.RIGHT ||
            crop_location == CropLocation.BOTTOM_RIGHT)
        {
            if (dx < min_size - new_w)
                dx = min_size - new_w;
            if (new_x + new_w + dx > pw)
                dx = pw - new_x - new_w;
        }
        if (crop_location == CropLocation.BOTTOM_LEFT ||
            crop_location == CropLocation.BOTTOM ||
            crop_location == CropLocation.BOTTOM_RIGHT)
        {
            if (dy < min_size - new_h)
                dy = min_size - new_h;
            if (new_y + new_h + dy > ph)
                dy = ph - new_y - new_h;
        }
        if (crop_location == CropLocation.MIDDLE)
        {
            if (new_x + dx + new_w > pw)
                dx = pw - new_x - new_w;
            if (new_x + dx < 0)
                dx = -new_x;
            if (new_y + dy + new_h > ph)
                dy = ph - new_y - new_h;
            if (new_y + dy  < 0)
                dy = -new_y;
        }

        /* Move crop */
        if (crop_location == CropLocation.MIDDLE)
        {
            new_x += dx;
            new_y += dy;
        }
        if (crop_location == CropLocation.TOP_LEFT ||
            crop_location == CropLocation.LEFT ||
            crop_location == CropLocation.BOTTOM_LEFT)
        {
            new_x += dx;
            new_w -= dx;
        }
        if (crop_location == CropLocation.TOP_LEFT ||
            crop_location == CropLocation.TOP ||
            crop_location == CropLocation.TOP_RIGHT)
        {
            new_y += dy;
            new_h -= dy;
        }

        if (crop_location == CropLocation.TOP_RIGHT ||
            crop_location == CropLocation.RIGHT ||
            crop_location == CropLocation.BOTTOM_RIGHT)
            new_w += dx;
        if (crop_location == CropLocation.BOTTOM_LEFT ||
            crop_location == CropLocation.BOTTOM ||
            crop_location == CropLocation.BOTTOM_RIGHT)
            new_h += dy;

        page.move_crop (new_x, new_y);

        /* If reshaped crop, must be a custom crop */
        if (new_w != cw || new_h != ch)
            page.set_custom_crop (new_w, new_h);
    }

    public void button_release (int x, int y)
    {
        /* Complete crop */
        crop_location = CropLocation.NONE;
        changed ();
    }

    private bool animation_cb ()
    {
        animate_segment = (animate_segment + 1) % animate_n_segments;
        changed ();
        return true;
    }

    private void update_animation ()
    {
        bool animate, is_animating;

        animate = page.is_scanning && !page.has_data;
        is_animating = animate_timeout != 0;
        if (animate == is_animating)
            return;

        if (animate)
        {
            animate_segment = 0;
            if (animate_timeout == 0)
                animate_timeout = Timeout.add (150, animation_cb);
        }
        else
        {
            if (animate_timeout != 0)
                Source.remove (animate_timeout);
            animate_timeout = 0;
        }
    }

    /* It is necessary to ask the ruler color since it is themed with the GTK */
    /* theme foreground color, and this class doesn't have any GTK widget     */
    /* available to lookup the color. */
    public void render (Cairo.Context context, Gdk.RGBA ruler_color)
    {
        update_animation ();
        update_page_view ();

        var w = get_preview_width ();
        var h = get_preview_height ();

        context.set_line_width (1);
        context.translate (x_offset, y_offset);

        /* Draw image */
        context.translate (border_width + ruler_width, border_width + ruler_width);
        Gdk.cairo_set_source_pixbuf (context, image, 0, 0);
        context.paint ();

        /* Draw page border */
        Gdk.cairo_set_source_rgba (context, ruler_color);
        context.set_line_width (border_width);

        context.rectangle (0,
                           0.0,
                           w,
                           h);
        context.stroke ();

        /* Draw horizontal ruler */
        context.set_line_width (1);
        var ruler_tick = 0;
        var line = 0.0;
        var big_ruler_tick = 5;

        while (ruler_tick <= page.width)
        {
            line = page_to_screen_x (ruler_tick) + 0.5;
            if (big_ruler_tick == 5)
            {
                context.move_to (line, 0);
                context.line_to (line, -ruler_width);
                context.move_to (line, h);
                context.line_to (line, h + ruler_width);
                big_ruler_tick = 0;
            }
            else
            {
                context.move_to (line, -2);
                context.line_to (line, -5);
                context.move_to (line, h + 2);
                context.line_to (line, h + 5);
            }
            ruler_tick = ruler_tick + page.dpi/5;
            big_ruler_tick = big_ruler_tick + 1;
        }
        context.stroke ();

        /* Draw vertical ruler */
        ruler_tick = 0;
        line = 0.0;
        big_ruler_tick = 5;
        while (ruler_tick <= page.height)
        {
            line = page_to_screen_y (ruler_tick) + 0.5;

            if (big_ruler_tick == 5)
            {
                context.move_to (0, line);
                context.line_to (-ruler_width, line);

                context.move_to (w, line);
                context.line_to (w + ruler_width, line);
                big_ruler_tick = 0;
            }
            else
            {
                context.move_to (-2, line);
                context.line_to (-5, line);

                context.move_to (w + 2, line);
                context.line_to (w + 5, line);
            }
            ruler_tick = ruler_tick + page.dpi/5;
            big_ruler_tick = big_ruler_tick + 1;
        }
        context.stroke ();

        /* Draw scan line */
        if (page.is_scanning && page.scan_line > 0)
        {
            var scan_line = page.scan_line;

            double s;
            double x1, y1, x2, y2;
            switch (page.scan_direction)
            {
            case ScanDirection.TOP_TO_BOTTOM:
                s = page_to_screen_y (scan_line);
                x1 = 0; y1 = s + 0.5;
                x2 = w; y2 = s + 0.5;
                break;
            case ScanDirection.BOTTOM_TO_TOP:
                s = page_to_screen_y (scan_line);
                x1 = 0; y1 = h - s + 0.5;
                x2 = w; y2 = h - s + 0.5;
                break;
            case ScanDirection.LEFT_TO_RIGHT:
                s = page_to_screen_x (scan_line);
                x1 = s + 0.5; y1 = 0;
                x2 = s + 0.5; y2 = h;
                break;
            case ScanDirection.RIGHT_TO_LEFT:
                s = page_to_screen_x (scan_line);
                x1 = w - s + 0.5; y1 = 0;
                x2 = w - s + 0.5; y2 = h;
                break;
            default:
                x1 = y1 = x2 = y2 = 0;
                break;
            }

            context.move_to (x1, y1);
            context.line_to (x2, y2);
            context.set_source_rgb (1.0, 0.0, 0.0);
            context.stroke ();
        }

        /* Draw crop */
        if (page.has_crop)
        {
            var x = page.crop_x;
            var y = page.crop_y;
            var crop_width = page.crop_width;
            var crop_height = page.crop_height;

            var dx = page_to_screen_x (x);
            var dy = page_to_screen_y (y);
            var dw = page_to_screen_x (crop_width);
            var dh = page_to_screen_y (crop_height);

            /* Shade out cropped area */
            context.rectangle (0, 0, w, h);
            context.new_sub_path ();
            context.rectangle (dx, dy, dw, dh);
            context.set_fill_rule (Cairo.FillRule.EVEN_ODD);
            context.set_source_rgba (0.25, 0.25, 0.25, 0.2);
            context.fill ();

            /* Show new edge */
            context.set_source_rgb (1.0, 1.0, 1.0);
            context.move_to (-border_width, dy - 1.5);
            context.line_to (border_width + w, dy - 1.5);
            context.move_to (-border_width, dy + dh + 1.5);
            context.line_to (border_width + w, dy + dh + 1.5);
            context.stroke ();

            context.move_to (dx - 1.5, -border_width);
            context.line_to (dx - 1.5, border_width + h);
            context.move_to (dx + dw + 1.5, -border_width);
            context.line_to (dx + dw + 1.5, border_width + h);
            context.stroke ();

            context.rectangle (dx - 0.5, dy - 0.5, dw + 1, dh + 1);
            context.set_source_rgb (0.0, 0.0, 0.0);
            context.stroke ();
        }
    }

    public int width
    {
        get { return width_; }
        set
        {
            // FIXME: Automatically update when get updated image
            var h = (int) ((double) value * page.height / page.width);
            if (width_ == value && height_ == h)
                return;

            width_ = value;
            height_ = h;

            /* Regenerate image */
            update_image = true;

            size_changed ();
            changed ();
        }
    }

    public int height
    {
        get { return height_; }
        set
        {
            // FIXME: Automatically update when get updated image
            var w = (int) ((double) value * page.width / page.height);
            if (width_ == w && height_ == value)
                return;

            width_ = w;
            height_ = value;

            /* Regenerate image */
            update_image = true;

            size_changed ();
            changed ();
        }
    }

    private void page_pixels_changed_cb (Page p)
    {
        /* Regenerate image */
        update_image = true;
        changed ();
    }

    private void page_size_changed_cb (Page p)
    {
        /* Regenerate image */
        update_image = true;
        size_changed ();
        changed ();
    }

    private void page_overlay_changed_cb (Page p)
    {
        changed ();
    }

    private void scan_direction_changed_cb (Page p)
    {
        /* Regenerate image */
        update_image = true;
        size_changed ();
        changed ();
    }
}