
/**
 * 支持的时间日期格式占位符:
 *  支持通用占位符和MySQL占位符(可以混合)
 *  格式化时以下都支持, 解析时不支持星期几
 *  不支持时区, 保留z Z zz ZZ zzz ZZZ zzzz ZZZZ %z %Z
 *
 * 4位年: yyyy %Y
 * 2位年: yy %y
 * 2位月: MM %m
 * 数字月: M %c
 * 英文月: MMMM %M
 * 简写英文月: MMM %b
 * 2位日: dd %d
 * 数字日: d
 * 2位时(24): HH %H
 * 数字时(24): H %k
 * 2位时(12): hh %I %h
 * 数字时(12): h %l
 * 2位分: mm ii %i
 * 数字分: m i
 * 2位秒: ss %s %S
 * 数字秒: s
 * 3位毫秒: sss
 * 小写上下午(AM/PM): a p
 * 大写上下午(AM/PM): A P %p
 * 星期几(周一为1, 周日为7): u
 * 星期几(周日为0): %w
 * 英文星期几: dddd %W
 * 简写英文星期几: ddd %a
 * %: %% // MySQL特殊占位符%
 */
// Eng/lish
const DATE_TIME_FORMAT_TYPE_IGNORE = 'ign';
const DATE_TIME_FORMAT_TYPE_EXPRESSION = 'exp';
const DATE_TIME_FORMAT_TYPE_RAW = 'raw';
const DAYS_SIMPLE = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const MONTHS_SIMPLE = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
function parseDateTimeFormatter(format)
{
  let expect = function(target, str, range) {
    let i = 0;
    if(!range)
      range = 1;
    if(!Array.isArray(range))
    {
      let arr = [];
      for(let m = 1; m <= range; m++)
        arr.push(m);
      range = arr;
    }
    let res = 0;
    let max = Math.max(...range);
    let min = Math.min(...range);
    while(i < str.length)
    {
      let ch = str[i++];
      if(target.indexOf(ch) === -1)
        break;
      if(range.indexOf(i) === -1)
      {
        if(i >= max)
          break;
      }
      if(range.indexOf(i) !== -1)
        res = i;
    }
    if(res > 0)
    {
      return res;
    }
    return false;
  };

  let i = 0;
  let arr = [];
  while(i < format.length)
  {
    let ch = format[i];
    let n = false;
    switch(ch)
    {
      case 'y':
        n = expect('y', format.substr(i), [2, 4]);
        break;
      case 'M':
        n = expect('M', format.substr(i), 4);
        break;
      case 'd':
        n = expect('d', format.substr(i), 4);
        break;
      case 'H':
      case 'h':
        n = expect(ch, format.substr(i), 2);
        break;
      case 'm':
      case 'i':
        n = expect(ch, format.substr(i), 2);
        break;
      case 's':
        n = expect('s', format.substr(i), 3);
        break;
      case 'a':
      case 'p':
      case 'A':
      case 'P':
      case 'u':
        n = expect(ch, format.substr(i));
        break;
      case 'z':
      case 'Z':
        n = expect(ch, format.substr(i), 4);
        break;
      case '%':
        n = expect('YmdHisSyecklIh%zZ', format.substr(i + 1));
        if(n === 1)
          n++;
        break;
      default:
        arr.push({str: ch, type: DATE_TIME_FORMAT_TYPE_RAW});
        i++;
        continue;
    }
    if(n)
    {
      let s = format.substr(i, n);
      let ignore = ['z', 'zz', 'zzz', 'zzzz', 'Z', 'ZZ', 'ZZZ', 'ZZZZ', '%z', '%Z'].indexOf(s) !== -1;
      arr.push({str: s, type: ignore ? DATE_TIME_FORMAT_TYPE_IGNORE : DATE_TIME_FORMAT_TYPE_EXPRESSION});
      i += n;
    }
    else
    {
      arr.push({str: format.substr(i), type: DATE_TIME_FORMAT_TYPE_RAW});
      break;
    }
  }
  return arr;
}

/**
 * 格式化日期时间
 * @param date 日期时间 Date对象/时间戳 默认当前日期时间
 * @param format 格式化字符串 字符串 默认 yyyy-MM-dd HH:mm:ss
 * @returns {string} 日期时间格式化后的字符串
 */
export function date_format(date, format) {
  if(arguments.length === 1)
  {
    format = date;
    date = null;
  }
  if (!date)
    date = new Date;
  else if(!date instanceof Date)
    date = new Date(date);
  format = format || 'yyyy-MM-dd HH:mm:ss';
  let numpad = function(num, limit) {
    limit = limit || 10;
    if(num >= limit)
      return num;
    let res = num;
    while(num < limit && limit >= 10)
    {
      res = '0' + res;
      limit /= 10;
    }
    return res;
  }
  const _y = date.getFullYear();
  const _m = date.getMonth() + 1;
  const _d = date.getDate();
  const _h = date.getHours();
  const _i = date.getMinutes();
  const _s = date.getSeconds();
  const _a = date.getDay();
  const _z = date.getMilliseconds();
  // General style
  let Options = {
    yyyy: _y,
    MM: numpad(_m),
    dd: numpad(_d),
    HH: numpad(_h),
    mm: numpad(_i),
    ss: numpad(_s),
    sss: numpad(_z, 100),
    yy: _y % 100,
    M: _m,
    d: _d,
    H: _h,
    m: _i,
    s: _s,
    hh: numpad((_h % 12) || 12),
    h: (_h % 12) || 12,
    // Qt style
    ddd: DAYS_SIMPLE[_a - 1],
    dddd: DAYS[_a - 1],
    MMM: MONTHS_SIMPLE[_m - 1],
    MMMM: MONTHS[_m - 1],
    // Java style
    a: _h > 12 ? 'pm' : 'am',
    A: _h > 12 ? 'PM' : 'AM',
    u: _a,
  };
  Options['p'] = Options.a;
  Options['P'] = Options.A;
  // PHP style
  Options['ii'] = Options.mm;
  Options['i'] = Options.m;
  // MySQL style
  Options['%Y'] = Options.yyyy;
  Options['%m'] = Options.MM;
  Options['%d'] = Options.dd;
  Options['%H'] = Options.HH;
  Options['%i'] = Options.mm;
  Options['%s'] = Options.ss;
  Options['%S'] = Options.ss;
  Options['%y'] = Options.yy;
  Options['%e'] = Options.d;
  Options['%c'] = Options.M;
  Options['%k'] = Options.H;
  Options['%l'] = Options.h;
  Options['%I'] = Options.hh;
  Options['%h'] = Options.hh;
  Options['%p'] = Options.A;
  Options['%w'] = Options.u - 1;
  Options['%a'] = Options.ddd;
  Options['%W'] = Options.dddd;
  Options['%M'] = Options.MMMM;
  Options['%b'] = Options.MMM;
  Options['%%'] = '%';

  let arr = parseDateTimeFormatter(format);
  //console.log(arr);
  arr = arr.filter((x) => x.type !== DATE_TIME_FORMAT_TYPE_IGNORE).map((x) => {
    return x.type === DATE_TIME_FORMAT_TYPE_EXPRESSION ? (Options[x.str] || x.str) : x.str;
  })
  //console.log(format, '|', arr.join(''));
  return arr.join('');
}

/**
 * 解析日期时间
 * @param date 日期时间字符串 字符串
 * @param format 格式化字符串 字符串 默认 yyyy-MM-dd HH:mm:ss
 * @param initNow 缺省部分是否使用当前日期时间部分代替 bool 默认不使用当前
 * @returns {Date} 解析日期时间出来的Date对象
 */
export function strtotime(date, format, initNow)
{
  if (!date)
    return null;
  let expect = function(target, str) {
    if(Array.isArray(target))
    {
      let res = 0;
      if(typeof(target[0]) === 'number')
      {
        for(let t of target)
        {
          if(str.length >= t)
          {
            if(t > res && new RegExp(`^\\d{${t}}`).test(str))
              res = t;
          }
        }
      }
      else
      {
        for(let t of target)
        {
          if(str.length < t.length)
            continue;
          if(str.startsWith(t))
          {
            res = t.length;
            break;
          }
        }
      }
      return res;
    }
    else
    {
      if(typeof(target) === 'number')
      {
        if(str.length < target)
          return 0;
        if(!new RegExp(`^\\d{${target}}`).test(str))
          return null;
        return target;
      }
      else if(typeof(target) === 'object')
      {
        let res = 0;
        let min = target.min || 0;
        let max = target.max;
        for(let m = min; m <= max; m++)
        {
          let t = '' + m;
          if(str.length >= t.length)
          {
            if(str.startsWith(t))
              res = t.length;
          }
        }
        return res;
      }
      else
      {
        if(str.length < target.length)
          return 0;
        if(!str.startsWith(target))
          return 0;
        return target.length;
      }
    }
  };
  format = format || 'yyyy-MM-dd HH:mm:ss';
  let arr = parseDateTimeFormatter(format);
  let i = 0;
  let _y = undefined;
  let _m = undefined;
  let _d = undefined;
  let _h = undefined;
  let _i = undefined;
  let _s = undefined;
  let _z = undefined;
  let _h_12 = undefined;
  let _ap = undefined;
  let get_token = function(str, target) {
    let r = expect(target, date.substr(i));
    if(r <= 0)
      return null;
    let nstr = date.substr(i, r);
    i += r;
    return nstr;
  };
  //console.log(arr);
  for(let part of arr)
  {
    if(part.type === DATE_TIME_FORMAT_TYPE_RAW || part.type === DATE_TIME_FORMAT_TYPE_IGNORE)
    {
      let r = expect(part.str, date.substr(i));
      if(!r)
        return null;
      i += r;
    }
    else
    {
      let p = null;
      switch(part.str)
      {
        case 'yyyy':
        case '%Y':
          p = get_token(part.str, 4);
          if(p)
            _y = parseInt(p);
          break;
        case 'yy':
        case '%y':
          p = get_token(part.str, 2);
          if(p)
            _y = Math.floor((date.getFullYear()) / 100) * 100 + parseInt(p);
          break;
        case 'MM':
        case '%m':
          p = get_token(part.str, 2);
          if(p)
            _m = parseInt(p);
          break;
        case 'M':
        case '%c':
          p = get_token(part.str, {min: 1, max: 12});
          if(p)
            _m = parseInt(p);
          break;
        case 'MMMM':
        case '%M':
          p = get_token(part.str, MONTHS);
          if(p)
            _m = MONTHS.indexOf(p) + 1;
          break;
        case 'MMM':
        case '%b':
          p = get_token(part.str, MONTHS_SIMPLE);
          if(p)
            _m = MONTHS_SIMPLE.indexOf(p) + 1;
          break;
        case 'dd':
        case '%d':
          p = get_token(part.str, 2);
          if(p)
            _d = parseInt(p);
          break;
        case 'd':
        case '%e':
          p = get_token(part.str, {min: 1, max: 31});
          if(p)
            _d = parseInt(p);
          break;
        case 'HH':
        case '%H':
          p = get_token(part.str, 2);
          if(p)
            _h = parseInt(p);
          break;
        case 'H':
        case '%k':
          p = get_token(part.str, {max: 23});
          if(p)
            _h = parseInt(p);
          break;
        case 'hh':
        case '%I':
        case '%h':
          p = get_token(part.str, 2);
          if(p)
            _h_12 = parseInt(p);
          break;
        case 'h':
        case '%l':
          p = get_token(part.str, {max: 12});
          if(p)
            _h_12 = parseInt(p);
          break;
        case 'mm':
        case 'ii':
        case '%i':
          p = get_token(part.str, 2);
          if(p)
            _i = parseInt(p);
          break;
        case 'm':
        case 'i':
          p = get_token(part.str, {max: 59});
          if(p)
            _i = parseInt(p);
          break;
        case 'ss':
        case '%s':
        case '%S':
          p = get_token(part.str, 2);
          if(p)
            _s = parseInt(p);
          break;
        case 's':
          p = get_token(part.str, {max: 59});
          if(p)
            _s = parseInt(p);
          break;
        case 'sss':
          p = get_token(part.str, {max: 999});
          if(p)
            _z = parseInt(p);
          break;
        case 'A':
        case 'p': {
          const AP = ['AM', 'PM'];
          p = get_token(part.str, AP);
          if(p)
            _ap = p.toLowerCase();
        }
          break;
        case 'a':
        case 'P':
        case '%p':{
          const AP = ['am', 'pm'];
          p = get_token(part.str, AP);
          if(p)
            _ap = p;
        }
          break;
        case 'u':
        case '%w':
        case '%W':
        case 'dddd':
        case 'ddd':
          throw new Error('不支持星期几解析: ' + part.str); // not ignore and throw
        //break;
        case '%%':
          //TODO: ignore 最好不解析出来
          break;
        default:
          throw new Error('不支持的格式解析: ' + part.str);
          break;
      }
      if(!p)
        return null;
    }
  }
  if(_h_12 !== undefined && _ap !== undefined)
  {
    _h = (_ap === 'pm' ? _h_12 + 12 : _h_12) % 24;
  }
  let res = new Date;
  //console.log('1111111111',_y, _m, _d, _h, _i, _s, _z);
  if(_y === undefined) _y = initNow ? res.getFullYear() : 0;
  if(_m === undefined) _m = initNow ? res.getMonth() + 1 : 1;
  if(_d === undefined) _d = initNow ? res.getDate() : 1;
  if(_h === undefined) _h = initNow ? res.getHours() : 0;
  if(_i === undefined) _i = initNow ? res.getMinutes() : 0;
  if(_s === undefined) _s = initNow ? res.getSeconds() : 0;
  if(_z === undefined) _z = initNow ? res.getMilliseconds() : 0;
  //console.log('2222222222',_y, _m, _d, _h, _i, _s, _z);
  res.setFullYear(_y, _m - 1, _d);
  res.setHours(_h, _i, _s, _z);
  return res;
}

/**
 * 转换日期时间格式
 * @param dateStr 源日期时间字符串 字符串
 * @param sourceFormat 源格式化字符串 字符串 默认 yyyy-MM-dd HH:mm:ss
 * @param targetFormat 目标格式化字符串 字符串 默认 yyyy-MM-dd HH:mm:ss
 * @param initNow 缺省部分是否使用当前日期时间部分代替 bool 默认不使用当前
 * @returns {string} 重新格式化的日期时间出字符串
 */
export function date(dateStr, sourceFormat, targetFormat, initNow)
{
  return date_format(strtotime(dateStr, sourceFormat, initNow), targetFormat);
}
