import { TimeDelta, Date, TimeZone, Time, DateTime } from '@silane/datetime';

import { BaseError } from './error';


function isPlainObject(value) {
  return value?.constructor === Object
}

export class SerializerError extends BaseError {
}

export function serialize(obj) {
  if(obj instanceof TimeDelta) {
    return {
      __type__: '@silane/datetime:TimeDelta',
      days: obj.days, seconds: obj.seconds, microseconds: obj.microseconds,
    };
  }
  if(obj instanceof DateTime) {
    if(obj.tzInfo && !obj.tzInfo instanceof TimeZone) {
      throw new SerializerError(
        'Cannot serialize "DateTime" object of @silane/datetime package because it has tzInfo value other than TimeZone'
      );
    }
    return {
      __type__: '@silane/datetime:DateTime',
      year: obj.year, month: obj.month, day: obj.day,
      hour: obj.hour, minute: obj.minute, second: obj.second,
      microsecond: obj.microsecond, fold: obj.fold,
      tzInfo: serialize(obj.tzInfo),
    };
  }
  if(obj instanceof Date) {
    return {
      __type__: '@silane/datetime:Date',
      year: obj.year, month: obj.month, day: obj.day,
    };
  }
  if(obj instanceof TimeZone) {
    return {
      __type__: '@silane/datetime:TimeZone',
      offset: serialize(obj.utcOffset()),
      name: obj.tzName(),
    };
  }
  if(obj instanceof Time) {
    if(obj.tzInfo && !obj.tzInfo instanceof TimeZone) {
      throw new SerializerError(
        'Cannot serialize "Time" object of @silane/datetime package because it has tzInfo value other than TimeZone'
      );
    }
    return {
      __type__: '@silane/datetime:Time',
      hour: obj.hour, minute: obj.minute, second: obj.second,
      microsecond: obj.microsecond, fold: obj.fold,
      tzInfo: serialize(obj.tzInfo),
    };
  }

  if(Array.isArray(obj)) {
    return obj.map(x => serialize(x));
  }
  if(isPlainObject(obj)) {
    if(obj.__type__ !== undefined) {
      throw new SerializerError(
        'Cannot serialize object including property "__type__".'
      );
    }
    return Object.fromEntries(
      [...Object.entries(obj)].map(x => [x[0], serialize(x[1])])
    );
  }
  if(obj instanceof Map) {
    return new Map(
      [...obj.entries()].map(x => [x[0], serialize(x[1])])
    );
  }
  return obj;
}

export function deserialize(obj) {
  if(isPlainObject(obj) && obj.__type__) {
    if(obj.__type__ === '@silane/datetime:TimeDelta') {
      return new TimeDelta({
        days: obj.days, seconds: obj.seconds, microseconds: obj.microseconds,
      });
    }
    if(obj.__type__ === '@silane/datetime:DateTime') {
      return new DateTime(
        obj.year, obj.month, obj.day, obj.hour, obj.minute, obj.second,
        obj.microsecond, deserialize(obj.tzInfo), obj.fold,
      );
    }
    if(obj.__type__ === '@silane/datetime:Date') {
      return new Date(obj.year, obj.month, obj.day);
    }
    if(obj.__type__ === '@silane/datetime:TimeZone') {
      return new TimeZone(obj.offset, obj.name);
    }
    if(obj.__type__ === '@silane/datetime:Time') {
      return new Time(
        obj.hour, obj.minute, obj.second,
        obj.microsecond, deserialize(obj.tzInfo), obj.fold,
      );
    }
    throw new SerializerError(`Unrecognized __type__: ${obj.__type__}`);
  }

  if(Array.isArray(obj)) {
    return obj.map(x => deserialize(x));
  }
  if(isPlainObject(obj)) {
    return Object.fromEntries(
      [...Object.entries(obj)].map(x => [x[0], deserialize(x[1])])
    );
  }
  if(obj instanceof Map) {
    return new Map(
      [...obj.entries()].map(x => [x[0], deserialize(x[1])])
    );
  }
  return obj;
}
