'use strict';

var calendars = require('./calendars');

var Lib = require('../../lib');
var constants = require('../../constants/numerical');

var EPOCHJD = constants.EPOCHJD;
var ONEDAY = constants.ONEDAY;

var attributes = {
    valType: 'enumerated',
    values: Lib.sortObjectKeys(calendars.calendars),
    editType: 'calc',
    dflt: 'gregorian'
};

var handleDefaults = function(contIn, contOut, attr, dflt) {
    var attrs = {};
    attrs[attr] = attributes;

    return Lib.coerce(contIn, contOut, attrs, attr, dflt);
};

var handleTraceDefaults = function(traceIn, traceOut, coords, layout) {
    for(var i = 0; i < coords.length; i++) {
        handleDefaults(traceIn, traceOut, coords[i] + 'calendar', layout.calendar);
    }
};

// each calendar needs its own default canonical tick. I would love to use
// 2000-01-01 (or even 0000-01-01) for them all but they don't necessarily
// all support either of those dates. Instead I'll use the most significant
// number they *do* support, biased toward the present day.
var CANONICAL_TICK = {
    chinese: '2000-01-01',
    coptic: '2000-01-01',
    discworld: '2000-01-01',
    ethiopian: '2000-01-01',
    hebrew: '5000-01-01',
    islamic: '1000-01-01',
    julian: '2000-01-01',
    mayan: '5000-01-01',
    nanakshahi: '1000-01-01',
    nepali: '2000-01-01',
    persian: '1000-01-01',
    jalali: '1000-01-01',
    taiwan: '1000-01-01',
    thai: '2000-01-01',
    ummalqura: '1400-01-01'
};

// Start on a Sunday - for week ticks
// Discworld and Mayan calendars don't have 7-day weeks but we're going to give them
// 7-day week ticks so start on our Sundays.
// If anyone really cares we can customize the auto tick spacings for these calendars.
var CANONICAL_SUNDAY = {
    chinese: '2000-01-02',
    coptic: '2000-01-03',
    discworld: '2000-01-03',
    ethiopian: '2000-01-05',
    hebrew: '5000-01-01',
    islamic: '1000-01-02',
    julian: '2000-01-03',
    mayan: '5000-01-01',
    nanakshahi: '1000-01-05',
    nepali: '2000-01-05',
    persian: '1000-01-01',
    jalali: '1000-01-01',
    taiwan: '1000-01-04',
    thai: '2000-01-04',
    ummalqura: '1400-01-06'
};

var DFLTRANGE = {
    chinese: ['2000-01-01', '2001-01-01'],
    coptic: ['1700-01-01', '1701-01-01'],
    discworld: ['1800-01-01', '1801-01-01'],
    ethiopian: ['2000-01-01', '2001-01-01'],
    hebrew: ['5700-01-01', '5701-01-01'],
    islamic: ['1400-01-01', '1401-01-01'],
    julian: ['2000-01-01', '2001-01-01'],
    mayan: ['5200-01-01', '5201-01-01'],
    nanakshahi: ['0500-01-01', '0501-01-01'],
    nepali: ['2000-01-01', '2001-01-01'],
    persian: ['1400-01-01', '1401-01-01'],
    jalali: ['1400-01-01', '1401-01-01'],
    taiwan: ['0100-01-01', '0101-01-01'],
    thai: ['2500-01-01', '2501-01-01'],
    ummalqura: ['1400-01-01', '1401-01-01']
};

/*
 * convert d3 templates to world-calendars templates, so our users only need
 * to know d3's specifiers. Map space padding to no padding, and unknown fields
 * to an ugly placeholder
 */
var UNKNOWN = '##';
var d3ToWorldCalendars = {
    d: {0: 'dd', '-': 'd'}, // 2-digit or unpadded day of month
    e: {0: 'd', '-': 'd'}, // alternate, always unpadded day of month
    a: {0: 'D', '-': 'D'}, // short weekday name
    A: {0: 'DD', '-': 'DD'}, // full weekday name
    j: {0: 'oo', '-': 'o'}, // 3-digit or unpadded day of the year
    W: {0: 'ww', '-': 'w'}, // 2-digit or unpadded week of the year (Monday first)
    m: {0: 'mm', '-': 'm'}, // 2-digit or unpadded month number
    b: {0: 'M', '-': 'M'}, // short month name
    B: {0: 'MM', '-': 'MM'}, // full month name
    y: {0: 'yy', '-': 'yy'}, // 2-digit year (map unpadded to zero-padded)
    Y: {0: 'yyyy', '-': 'yyyy'}, // 4-digit year (map unpadded to zero-padded)
    U: UNKNOWN, // Sunday-first week of the year
    w: UNKNOWN, // day of the week [0(sunday),6]
    // combined format, we replace the date part with the world-calendar version
    // and the %X stays there for d3 to handle with time parts
    c: {0: 'D M d %X yyyy', '-': 'D M d %X yyyy'},
    x: {0: 'mm/dd/yyyy', '-': 'mm/dd/yyyy'}
};

function worldCalFmt(fmt, x, calendar) {
    var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD;
    var cDate = getCal(calendar).fromJD(dateJD);
    var i = 0;
    var modifier, directive, directiveLen, directiveObj, replacementPart;

    while((i = fmt.indexOf('%', i)) !== -1) {
        modifier = fmt.charAt(i + 1);
        if(modifier === '0' || modifier === '-' || modifier === '_') {
            directiveLen = 3;
            directive = fmt.charAt(i + 2);
            if(modifier === '_') modifier = '-';
        } else {
            directive = modifier;
            modifier = '0';
            directiveLen = 2;
        }
        directiveObj = d3ToWorldCalendars[directive];
        if(!directiveObj) {
            i += directiveLen;
        } else {
            // code is recognized as a date part but world-calendars doesn't support it
            if(directiveObj === UNKNOWN) replacementPart = UNKNOWN;

            // format the cDate according to the translated directive
            else replacementPart = cDate.formatDate(directiveObj[modifier]);

            fmt = fmt.substr(0, i) + replacementPart + fmt.substr(i + directiveLen);
            i += replacementPart.length;
        }
    }
    return fmt;
}

// cache world calendars, so we don't have to reinstantiate
// during each date-time conversion
var allCals = {};
function getCal(calendar) {
    var calendarObj = allCals[calendar];
    if(calendarObj) return calendarObj;

    calendarObj = allCals[calendar] = calendars.instance(calendar);
    return calendarObj;
}

function makeAttrs(description) {
    return Lib.extendFlat({}, attributes, { description: description });
}

function makeTraceAttrsDescription(coord) {
    return 'Sets the calendar system to use with `' + coord + '` date data.';
}

var xAttrs = {
    xcalendar: makeAttrs(makeTraceAttrsDescription('x'))
};

var xyAttrs = Lib.extendFlat({}, xAttrs, {
    ycalendar: makeAttrs(makeTraceAttrsDescription('y'))
});

var xyzAttrs = Lib.extendFlat({}, xyAttrs, {
    zcalendar: makeAttrs(makeTraceAttrsDescription('z'))
});

var axisAttrs = makeAttrs([
    'Sets the calendar system to use for `range` and `tick0`',
    'if this is a date axis. This does not set the calendar for',
    'interpreting data on this axis, that\'s specified in the trace',
    'or via the global `layout.calendar`'
].join(' '));

module.exports = {
    moduleType: 'component',
    name: 'calendars',

    schema: {
        traces: {
            scatter: xyAttrs,
            bar: xyAttrs,
            box: xyAttrs,
            heatmap: xyAttrs,
            contour: xyAttrs,
            histogram: xyAttrs,
            histogram2d: xyAttrs,
            histogram2dcontour: xyAttrs,
            scatter3d: xyzAttrs,
            surface: xyzAttrs,
            mesh3d: xyzAttrs,
            scattergl: xyAttrs,
            ohlc: xAttrs,
            candlestick: xAttrs
        },
        layout: {
            calendar: makeAttrs([
                'Sets the default calendar system to use for interpreting and',
                'displaying dates throughout the plot.'
            ].join(' '))
        },
        subplots: {
            xaxis: {calendar: axisAttrs},
            yaxis: {calendar: axisAttrs},
            scene: {
                xaxis: {calendar: axisAttrs},
                // TODO: it's actually redundant to include yaxis and zaxis here
                // because in the scene attributes these are the same object so merging
                // into one merges into them all. However, I left them in for parity with
                // cartesian, where yaxis is unused until we Plotschema.get() when we
                // use its presence or absence to determine whether to delete attributes
                // from yaxis if they only apply to x (rangeselector/rangeslider)
                yaxis: {calendar: axisAttrs},
                zaxis: {calendar: axisAttrs}
            },
            polar: {
                radialaxis: {calendar: axisAttrs}
            }
        },
        transforms: {
            filter: {
                valuecalendar: makeAttrs([
                    'WARNING: All transforms are deprecated and may be removed from the API in next major version.',
                    'Sets the calendar system to use for `value`, if it is a date.'
                ].join(' ')),
                targetcalendar: makeAttrs([
                    'WARNING: All transforms are deprecated and may be removed from the API in next major version.',
                    'Sets the calendar system to use for `target`, if it is an',
                    'array of dates. If `target` is a string (eg *x*) we use the',
                    'corresponding trace attribute (eg `xcalendar`) if it exists,',
                    'even if `targetcalendar` is provided.'
                ].join(' '))
            }
        }
    },

    layoutAttributes: attributes,

    handleDefaults: handleDefaults,
    handleTraceDefaults: handleTraceDefaults,

    CANONICAL_SUNDAY: CANONICAL_SUNDAY,
    CANONICAL_TICK: CANONICAL_TICK,
    DFLTRANGE: DFLTRANGE,

    getCal: getCal,
    worldCalFmt: worldCalFmt
};
