// vim: et:ts=4:sw=4:sts=4:ft=js

/*@
Ox.Calendar <f:Ox.Element> Basic calendar object
    () ->              <f> Calendar object
    (options) ->       <f> Calendar object
    (options, self) -> <f> Calendar object
    options <o> Options object
        date    <d|new Date()> UTC Date on which the calendar is centered
        dates   <[o]|[]> Date objects to be displayed
            end     <s> End of the event (UTC Date, as string)
            name    <s> Name of the event
            start   <s> Start of the event (UTC Date, as string)
            type    <s> Type of the event (like "person")
        height  <n|256> Height in px
        range   <[n]|[100, 5101]> Start and end year of the calendar
        width   <n|256> Width in px
        zoom    <n|8> Initial zoom level
    self    <o> Shared private variable
@*/

// Fixme: switch to UTC
// Fixme: in options.dates, replace "stop" with "end"
// Fixme: create a variable-resolution date type (with end that is _inclusive_)

Ox.Calendar = function(options, self) {

    self = self || {};
    var that = new Ox.Element({}, self)
        .defaults({
            date: new Date(),
            dates: [],
            height: 256,
            range: [1000, 3000],
            width: 256,
            zoom: 8
        })
        .options(options || {})
        .addClass('OxCalendar')
        .css({
            width: self.options.width + 'px',
            height: self.options.height + 'px'
        });

    self.maxZoom = 32;
    self.minLabelWidth = 80;
    self.overlayWidths = [Math.round(self.options.width / 16)];
    self.overlayWidths = [
        Math.floor((self.options.width - self.overlayWidths[0]) / 2),
        self.overlayWidths[0],
        Math.ceil((self.options.width - self.overlayWidths[0]) / 2),
    ];
    self.units = [
        {
            id: 'millennium',
            seconds: 365242.5 * 86400,
            date: function(i) {
                return '01/01/' + (i + 1) + '000 UTC';
            },
            name: function(i) {
                return Ox.formatOrdinal(i + 2) + ' millennium';
            },
            value: function(date) {
                return Math.floor(date.getUTCFullYear() / 1000) - 1;
            }
        },
        {
            id: 'century',
            seconds: 36524.25 * 86400,
            date: function(i) {
                return '01/01/' + (i + 19) + '00 UTC';
            },
            name: function(i) {
                return Ox.formatOrdinal(i + 20) + ' century';
            },
            value: function(date) {
                return Math.floor(date.getUTCFullYear() / 100) - 19;
            }
        },
        {
            id: 'decade',
            seconds: 3652.425 * 86400,
            date: function(i) {
                return '01/01/' + (i + 197) + '0 UTC'
            },
            name: function(i) {
                return (i + 197) + '0s'
            },
            value: function(date) {
                return Math.floor(date.getUTCFullYear() / 10) - 197;
            }
        },
        {
            id: 'year',
            seconds: 365.2425 * 86400,
            date: function(i) {
                return '01/01/' + (i + 1970) + ' UTC';
            },
            name: function(i) {
                return (i + 1970) + '';
            },
            value: function(date) {
                return date.getUTCFullYear() - 1970;
            }
        },
        {
            id: 'month',
            seconds: 365.2425 / 12 * 86400,
            date: function(i) {
                return (Ox.mod(i, 12) + 1) + '/01/' + (Math.floor(i / 12) + 1970) + ' UTC';
            },
            name: function(i) {
                return Ox.SHORT_MONTHS[Ox.mod(i, 12)] + ' ' + Math.floor(i / 12 + 1970)
            },
            value: function(date) {
                return (date.getUTCFullYear() - 1970) * 12 + date.getUTCMonth();
            }
        },
        {
            id: 'week',
            seconds: 7 * 86400,
            date: function(i) {
                return (i * 7 - 3) * 86400000;
            },
            name: function(i) {
                return Ox.formatDate(new Date((i * 7 - 3) * 86400000), '%a, %b %e');
            },
            value: function(date) {
                return Math.floor((date / 86400000 + 4) / 7);
            }
        },
        {
            id: 'day',
            seconds: 86400,
            date: function(i) {
                return i * 86400000;
            },
            name: function(i) {
                return Ox.formatDate(new Date(i * 86400000), '%b %e, %Y', true);
            },
            value: function(date) {
                return Math.floor(date / 86400000);
            }
        },
        {
            id: 'six_hours',
            seconds: 21600,
            date: function(i) {
                return i * 21600000;
            },
            name: function(i) {
                return Ox.formatDate(new Date(i * 21600000), '%b %e, %H:00', true);
            },
            value: function(date) {
                return Math.floor(date / 21600000);
            }
        },
        {
            id: 'hour',
            seconds: 3600,
            date: function(i) {
                return i * 3600000;
            },
            name: function(i) {
                return Ox.formatDate(new Date(i * 3600000), '%b %e, %H:00', true);
            },
            value: function(date) {
                return Math.floor(date / 3600000);
            }
        },
        {
            id: 'five_minutes',
            seconds: 300,
            date: function(i) {
                return i * 300000;
            },
            name: function(i) {
                return Ox.formatDate(new Date(i * 300000), '%b %e, %H:%M', true);
            },
            value: function(date) {
                return Math.floor(date / 300000);
            }
        },
        {
            id: 'minute',
            seconds: 60,
            date: function(i) {
                return i * 60000;
            },
            name: function(i) {
                return Ox.formatDate(new Date(i * 60000), '%b %e, %H:%M', true);
            },
            value: function(date) {
                return Math.floor(date / 60000);
            }
        },
        {
            id: 'five_seconds',
            seconds: 5,
            date: function(i) {
                return i * 5000;
            },
            name: function(i) {
                return Ox.formatDate(new Date(i * 5000), '%H:%M:%S', true);
            },
            value: function(date) {
                return Math.floor(date / 5000);
            }
        },
        {
            id: 'second',
            seconds: 1,
            date: function(i) {
                return i * 1000;
            },
            name: function(i) {
                return Ox.formatDate(new Date(i * 1000), '%H:%M:%S', true);
            },
            value: function(date) {
                return Math.floor(date / 1000);
            }
        }
    ];

    self.$container = new Ox.Element()
        .addClass('OxCalendarContainer')
        .css({
            top: '24px',
            bottom: '40px'
        })
        .bind({
            mouseleave: mouseleave,
            mousemove: mousemove,
            mousewheel: mousewheel
        })
        .bindEvent({
            doubleclick: doubleclick,
            dragstart: dragstart,
            drag: drag,
            dragpause: dragpause,
            dragend: dragend,
            singleclick: singleclick
        })
        .appendTo(that);

    self.$content = new Ox.Element()
        .addClass('OxCalendarContent')
        .appendTo(self.$container);

    self.$background = new Ox.Element()
        .addClass('OxBackground')
        .appendTo(self.$content);

    self.$scalebar = new Ox.Element()
        .addClass('OxTimeline')
        .css({
            posision: 'absolute',
        })
        .appendTo(self.$content);

    self.$scrollbar = new Ox.Element()
        .addClass('OxTimeline')
        .css({
            posision: 'absolute',
            bottom: '40px'
        })
        .appendTo(that);
    self.$overlay = new Ox.Element()
        .addClass('OxOverlay')
        .css({
            bottom: '40px'
        })
        .append(
            $('<div>').css({
                width: self.overlayWidths[0] + 'px'
            })
        )
        .append(
            $('<div>').css({
                left:  self.overlayWidths[0] + 'px',
                width: self.overlayWidths[1] + 'px'
            })
        )
        .append(
            $('<div>').css({
                left: (self.overlayWidths[0] + self.overlayWidths[1]) + 'px',
                width: self.overlayWidths[2] + 'px'
            })
        )
        .bindEvent({
            dragstart: dragstartScrollbar,
            drag: dragScrollbar,
            dragpause: dragpauseScrollbar,
            dragend: dragendScrollbar
        })
        .appendTo(that);

    self.$zoombar = new Ox.Element()
        .css({
            position: 'absolute',
            bottom: 24 + 'px',
            height: '16px'
        })
        .appendTo(that);
    self.$zoomInput = new Ox.Range({
            arrows: true,
            max: self.maxZoom,
            min: 0,
            size: self.options.width,
            thumbSize: 32,
            thumbValue: true,
            value: self.options.zoom
        })
        .bindEvent({
            change: changeZoom
        })
        .appendTo(self.$zoombar);

    self.$statusbar = new Ox.Bar({
            size: 24
        })
        .css({
            // fixme: no need to set position absolute with map statusbar
            position: 'absolute',
            bottom: 0,
            textAlign: 'center'
        })
        .appendTo(that);

    self.$tooltip = new Ox.Tooltip({
            animate: false
        })
        .css({
            textAlign: 'center'
        });
    
    renderCalendar();

    function changeDate() {
        
    }

    function changeZoom(event, data) {
        self.options.zoom = data.value;
        renderCalendar();
    }

    function doubleclick(event, e) {
        if ($(e.target).is(':not(.OxLine > .OxDate)')) {
            if (self.options.zoom < self.maxZoom) {
                self.options.date = new Date(
                    (+self.options.date + +getMouseDate(e)) / 2
                );
                self.options.zoom++;
            }
            renderCalendar();
        }
    }

    function dragstart(event, e) {
        if ($(e.target).is(':not(.OxLine > .OxDate)')) {
            self.drag = {x: e.clientX};
        }
    }

    function drag(event, e) {
        if (self.drag) {
            ///*
            self.$content.css({
                marginLeft: (e.clientX - self.drag.x) + 'px'
            });
            self.$scrollbar.css({
                marginLeft: Math.round((e.clientX - self.drag.x) / 16) + 'px'
            });
            //*/
            /*
            self.options.date = new Date(
                +self.options.date - e.clientDX * getSecondsPerPixel() * 1000
            );
            */
            //self.drag = {x: e.clientX};
        }
    }

    function dragpause(event, e) {
        if (self.drag) {
            dragafter(e);
            self.drag = {x: e.clientX};
        }
    }

    function dragend(event, e) {
        if (self.drag) {
            dragafter(e);
            self.drag = null;
        }
    }

    function dragafter(e) {
        self.options.date = new Date(
            +self.options.date - (e.clientX - self.drag.x) * getSecondsPerPixel() * 1000
        );
        self.$content.css({
            marginLeft: 0
        });
        self.$scrollbar.css({
            marginLeft: 0
        });
        renderCalendar();
    }

    function dragstartScrollbar(event, e) {
        self.drag = {x: e.clientX};
    }

    function dragScrollbar(event, e) {
        self.$content.css({
            marginLeft: ((e.clientX - self.drag.x) * 16) + 'px'
        });
        self.$scrollbar.css({
            marginLeft: (e.clientX - self.drag.x) + 'px'
        });
    }

    function dragpauseScrollbar(event, e) {
        dragafterScrollbar(e);
        self.drag = {x: e.clientX};
    }

    function dragendScrollbar(event, e) {
        dragafterScrollbar(e);
        self.drag = null;
    }

    function dragafterScrollbar(e) {
        self.options.date = new Date(
            +self.options.date + (self.drag.x - e.clientX) * getSecondsPerPixel() * 1000 * 16
        );
        // fixme: duplicated
        self.$content.css({
            marginLeft: 0
        });
        self.$scrollbar.css({
            marginLeft: 0
        });
        renderCalendar();
    }

    function formatDate(date) {
        var isFullDays = Ox.formatDate(date.start, '%H:%M:%S') == '00:00:00' &&
                Ox.formatDate(date.stop, '%H:%M:%S') == '00:00:00',
            isOneDay = isFullDays && date.stop - date.start == 86400000, // fixme: wrong, DST
            isSameDay = Ox.formatDate(date.start, '%Y-%m-%d') ==
                Ox.formatDate(date.stop, '%Y-%m-%d'),
            isSameYear = date.start.getFullYear() == date.stop.getFullYear(),
            timeFormat = isFullDays ? '' : ', %H:%M:%S',
            str = Ox.formatDate(date.start, '%a, %b %e');
        if (isOneDay || isSameDay || !isSameYear) {
            str += Ox.formatDate(date.start, ', %Y' + timeFormat);
        }
        if (!isOneDay && !isSameDay) {
            str += Ox.formatDate(date.stop, ' - %a, %b %e, %Y' + timeFormat);
        }
        if (isSameDay) {
            str += Ox.formatDate(date.stop, ' - ' + timeFormat.replace(', ', ''));
        }
        return str;
    }

    function getCalendarDate() {
        var ms = self.options.width * getSecondsPerPixel() * 1000;
        return {
            start: new Date(+self.options.date - ms / 2),
            stop: new Date(+self.options.date + ms / 2)
        };
    }

    function getDateByName(name) {
        var date = {};
        Ox.forEach(self.options.dates, function(v) {
            if (v.name == name) {
                date = v;
                return false;
            }
        });
        return date;
    }

    function getDateElement(date, zoom) {
        var left = getPosition(date.start, zoom),
            width = Math.max(getPosition(date.stop, zoom) - left, 1);
        return new Ox.Element()
            .addClass('OxDate')
            .css({
                left: left + 'px',
                width: width + 'px'
            })
            .data({
                name: date.name
            })
            .html('&nbsp;' + date.name);
    }

    function getMouseDate(e) {
        return new Date(+self.options.date + (
            e.clientX - that.offset().left - self.options.width / 2 - 1
        ) * getSecondsPerPixel() * 1000);            
    }

    function getPixelsPerSecond(zoom) {
        return Math.pow(2, (zoom || self.options.zoom) - (self.maxZoom - 4));
    }

    function getPosition(date, zoom) {
        return Math.round(
            self.options.width / 2 + 
            (date - self.options.date) / 1000 *
            getPixelsPerSecond(zoom || self.options.zoom)
        );
    }

    function getSecondsPerPixel(zoom) {
        return 1 / getPixelsPerSecond(zoom);
    }

    function getBackgroundElements(zoom) {
        // fixme: duplicated
        var $elements = [],
            units = getUnits(zoom),
            n, value, width;
        [1, 0].forEach(function(u) {
            var unit = units[u],
                value = unit.value(self.options.date),
                width = unit.seconds * getPixelsPerSecond(zoom),
                n = Math.ceil(self.options.width * 1.5/* * 16*/ / width);
                Ox.loop(-n, n + 1, function(i) {
                    $elements.push(
                        new Ox.Element()
                            .addClass(
                                u == 0 ? 'line' : Ox.mod(value + i, 2) == 0 ? 'even' : 'odd'
                            )
                            .css({
                                left: getPosition(new Date(unit.date(value + i)), zoom) + 'px',
                                width: (u == 0 ? 1 : width) + 'px'
                            })
                    );
                });
        });
        return $elements;
    }

    function getTimelineElements(zoom) {
        var $elements = [],
            unit = getUnits(zoom)[0],
            value = unit.value(self.options.date),
            width = unit.seconds * getPixelsPerSecond(zoom);
            n = Math.ceil(self.options.width * 1.5/* * 16*/ / width);
        Ox.loop(-n, n + 1, function(i) {
            $elements.push(
                getDateElement({
                        name: unit.name(value + i),
                        start: new Date(unit.date(value + i)),
                        stop: new Date(unit.date(value + i + 1))
                    }, zoom)
                    .addClass(Ox.mod(value + i, 2) == 0 ? 'even' : 'odd')
            );
        });
        return $elements;
    }

    function getUnits(zoom) {
        // returns array of 2 units
        // units[0] for timeline
        // units[1] for background
        var pixelsPerSecond = getPixelsPerSecond(zoom),
            units;
        self.units = self.units.reverse();
        Ox.forEach(self.units, function(v, i) {
            width = Math.round(v.seconds * pixelsPerSecond);
            if (width >= self.minLabelWidth) {
                units = [self.units[i], self.units[i - 1]];
                return false;
            }
        });
        self.units = self.units.reverse();
        return units;
    }

    function mouseleave() {
        self.$tooltip.hide();
    }

    function mousemove(e) {
        var $target = $(e.target),
            date, title;
        if ($target.is('.OxLine > .OxDate')) {
            date = getDateByName($target.data('name'));
            title = '<span class="OxBright">' + date.name + '</span><br/>' +
                formatDate(date);
        } else {
            title = Ox.formatDate(getMouseDate(e), '%a, %b %e, %Y, %H:%M:%S');
        }
        self.$tooltip.options({
                title: title
            })
            .show(e.clientX, e.clientY);
    }

    function mousewheel(e, delta, deltaX, deltaY) {
        //Ox.print('mousewheel', delta, deltaX, deltaY);
        var deltaZ = 0;
        if (!self.mousewheel && deltaY && Math.abs(deltaY) > Math.abs(deltaX)) {
            if (deltaY < 0 && self.options.zoom > 0) {
                deltaZ = -1
            } else if (deltaY > 0 && self.options.zoom < self.maxZoom) {
                deltaZ = 1
            }
            if (deltaZ) {
                self.options.date = deltaZ == -1 ?
                    new Date(2 * +self.options.date - +getMouseDate(e)) :
                    new Date((+self.options.date + +getMouseDate(e)) / 2)
                self.options.zoom += deltaZ;
                self.$zoomInput.options({value: self.options.zoom});
                renderCalendar();
            }
        }
        self.mousewheel = true;
        setTimeout(function() {
            self.mousewheel = false;
        }, 250);
    }

    function overlaps(date0, date1) {
        return (
            date0.start >= date1.start && date0.start < date1.stop
        ) || (
            date1.start >= date0.start && date1.start < date0.stop
        );
    }

    function renderCalendar() {
        $('.OxBackground').empty();
        $('.OxDate').remove();
        renderBackground();
        renderTimelines();
        renderDates();
        self.$statusbar.html(
            Ox.formatDate(self.options.date, '%a, %b %e, %Y, %H:%M:%S (%s)', true)
        );
    }

    function renderBackground() {
        getBackgroundElements(self.options.zoom).forEach(function($element) {
            $element.appendTo(self.$background);
        });
    }

    function renderDates() {
        var calendarDate = getCalendarDate();
            lineDates = [];
        self.options.dates.filter(function(date) {
            // filter out dates outside the visible area
            return overlaps(date, calendarDate);
        }).sort(function(a, b) {
            // sort dates by duration, descending
            return (b.stop - b.start) - (a.stop - a.start);
        }).forEach(function(date, i) {
            var line = lineDates.length;
            // traverse lines
            Ox.forEach(lineDates, function(dates, line_) {
                var fits = true;
                // traverse dates in line
                Ox.forEach(dates, function(date_) {
                    // if overlaps, check next line
                    if (overlaps(date, date_)) {
                        fits = false;
                        return false;
                    }
                });
                if (fits) {
                    line = line_;
                    return false;
                }
            });
            if (line == lineDates.length) {
                lineDates[line] = [];
            }
            lineDates[line].push(date);
        });
        $('.OxLine').remove();
        lineDates.forEach(function(dates, line) {
            var $line = new Ox.Element()
                .addClass('OxLine')
                .css({
                    top: ((line + 1) * 16) + 'px'
                })
                .appendTo(self.$content);
            dates.sort(function(a, b) {
                // sort dates by start, ascending
                return a.start - b.start;
            }).forEach(function(date) {
                getDateElement(date).appendTo($line);
            });
        });
    }

    function renderTimelines() {
        Ox.print(self.options.zoom, Math.max(self.options.zoom - 4, 0))
        getTimelineElements(self.options.zoom).forEach(function($element) {
            $element.appendTo(self.$scalebar.$element);
        });
        getTimelineElements(Math.max(self.options.zoom - 4, 0)).forEach(function($element) {
            $element.appendTo(self.$scrollbar.$element);
        });
    }

    function singleclick(event, e) {
        if ($(e.target).is(':not(.OxLine > .OxDate)')) {
            self.options.date = getMouseDate(e);
            renderCalendar();
        }
    }

    self.setOption = function(key, val) {
        if (key == 'date') {
            
        } else if (key == 'zoom') {
            
        }
    };

    return that;

};
