// TRICK: https://ultimatecourses.com/blog/understanding-javascript-types-and-reliable-type-checking#true-object-types
export const classof = (value) => Object.prototype.toString.call(value).slice(8, -1);
export const isString = (value) => classof(value) === 'String';
export const isDate = (value) => classof(value) === 'Date';
export const isRegExp = (value) => classof(value) === 'RegExp';
export const isNumber = (value) => classof(value) === 'Number' && !Number.isNaN(value);
export const isNaN = (value) => Number.isNaN(value);
export const isInteger = (value) => Number.isInteger(value);
export const isBoolean = (value) => classof(value) === 'Boolean';
export const isNull = (value) => classof(value) === 'Null';
export const isUndefined = (value) => classof(value) === 'Undefined';
export const isFunction = (value) => classof(value) === 'Function';
export const isArray = (value) => classof(value) === 'Array';
export const isObject = (value) => classof(value) === 'Object';
export const isSymbol = (value) => classof(value) === 'Symbol';
export const isMap = (value) => classof(value) === 'Map';
export const isSet = (value) => classof(value) === 'Set';
export const isWeakMap = (value) => classof(value) === 'WeakMap';
export const isWeakSet = (value) => classof(value) === 'WeakSet';

// export function parseValue(n) {
//   const type = typeof n;
//   if (!['string', 'number'].includes(type)) {
//     throw Error(`invalid type for CSS value: ${type}`);
//   }
//   n = type === 'string' ? n.trim() : n;
//   const value = parseFloat(n);
//   if (Number.isNaN(value)) return { value: n };
//   const unit = type === 'string' ? n.replace(value, '') : '';
//   return { value, unit };
// }

// export function modifyValue(n, fn) {
//   n = typeof n === 'string' ? n.trim() : n;
//   const number = parseFloat(n);
//   if (Number.isNaN(number)) return n;
//   const unit = String(n).replace(number, '');
//   return `${fn ? fn(number) : number}${unit || 'px'}`;
// }

// export function assertUnit(n, unit = 'px') {
//   n = typeof n === 'string' ? n.trim() : n;
//   const number = parseFloat(n);
//   if (Number.isNaN(number)) return n;
//   return `${number}${unit}`;
// }

export const compressObj = (obj) => {
  return Object.keys(obj).reduce((o, k) => {
    if (obj[k] !== undefined) o[k] = obj[k];
    return o;
  }, {});
};

// prettier-ignore
const allStylePropKeys = [
  'm', 'mt', 'mr', 'mb', 'ml', 'my', 'mx',
  'p', 'pt', 'pr', 'pb', 'pl', 'py', 'px',
  'gap', 'gapY', 'gapX',
  'w', 'h',
  'f', 'ff', 'fz', 'fw', 'fs',
  'cols', 'span', 'start', 'end', 'push', 'pull', 'pushR', 'pullR',
  'rows', 'rowSpan', 'rowStart', 'rowEnd',
];

// these are the only keys for which we parse units or keywords
// prettier-ignore
const dimensionalStylePropKeys = [
  'm', 'mt', 'mr', 'mb', 'ml', 'my', 'mx',
  'p', 'pt', 'pr', 'pb', 'pl', 'py', 'px',
  'gap', 'gapY', 'gapX',
  'w', 'h',
]

// prettier-ignore
export const normalizeStyleProps = ({
  f,
  ff=(f?f.split('-')[0]:undefined),
  fz=(f?f.split('-')[1]:undefined),
  m, p,
  my=m, mt=my, mb=my,
  mx=m, mr=mx, ml=mx,
  py=p, pt=py, pb=py,
  px=p, pr=px, pl=px,
  gap, gapY=gap, gapX=gap,
  span, start, end,
  push, pull, pushR, pullR,
  w,
  ...rest
} = {}) => ({
  ff, fz,
  pr, pl,
  mt, mb,
  pt, pb,
  gapY, gapX,
  span, start, end,
  push, pull, pushR, pullR,
  // NOTE: mr, ml and w styleProps will be overridden by certain others
  mr: pushR||pullR||start||end ? undefined : mr,
  ml: push||pull||start||end ? undefined : ml,
  w: span||start||end ? undefined : w,
  ...rest
});

export const outerSpacingVar = (key, val) => {
  return (
    {
      t: 'var(--outer-top)',
      r: 'var(--outer-right)',
      b: 'var(--outer-bottom)',
      l: 'var(--outer-left)',
    }[key.slice(key.length - 1).toLowerCase()] || val
  );
};

export const innerSpacingVar = (key, val) => {
  return (
    {
      t: `var(--inner-y-${val})`,
      r: `var(--inner-x-${val})`,
      b: `var(--inner-y-${val})`,
      l: `var(--inner-x-${val})`,
    }[key.slice(key.length - 1).toLowerCase()] || val
  );
};

export function outputValue(key, val) {
  if (!~dimensionalStylePropKeys.indexOf(key)) return val;
  val = typeof val === 'string' ? val.trim() : val;
  const number = parseFloat(val);
  if (Number.isNaN(number)) {
    if (val == 'frame') return outerSpacingVar(key, val);
    if (val == 'bleed') return `calc(${outerSpacingVar(key, val)}/-1)`;
    return val.startsWith('neg-')
      ? `calc(${innerSpacingVar(key, val.slice(4))}/-1)`
      : innerSpacingVar(key, val);
  }
  const unit = String(val).replace(number, '');
  return `${number}${unit || 'px'}`;
}

// get css variable name from style-prop name, e.g. rowSpan -> row-span
export const propVarName = (propName) =>
  ({
    rowSpan: 'row-span',
    rowStart: 'row-start',
    rowEnd: 'row-end',
    span: 'col-span',
    start: 'col-start',
    end: 'col-end',
    gapY: 'gap-y',
    gapX: 'gap-x',
  }[propName] || propName);

export const propClassName = (propName) =>
  ({
    span: 'span',
    start: 'start',
    end: 'end',
  }[propName] ||
  propVarName(propName) ||
  propName);

export function reduceStyleProps({ style = {}, className = '', ...restProps } = {}) {
  const inputProps = normalizeStyleProps(restProps);
  const outputProps = {},
    classes = className.split(/\s+/).filter(Boolean);
  Object.keys(inputProps).reduce(
    ([sObj, cArr, pObj], key) => {
      const styleProp = inputProps[key];
      if (styleProp === undefined) return [sObj, cArr, pObj];
      if (~allStylePropKeys.indexOf(key)) {
        cArr.push(propClassName(key));
        if (!isObject(styleProp)) {
          sObj[`--${propVarName(key)}`] = outputValue(key, styleProp);
        } else {
          const bps = Object.keys(styleProp);
          const [str] = bps.reduce(
            ([str, obj], bp) => {
              if (bp !== '_') {
                obj[`--${bp}__${propVarName(key)}`] = `var(--is-bp__${bp}) ${outputValue(
                  key,
                  styleProp[bp],
                )}`;
              }
              return [
                bp == '_'
                  ? str
                  : str.length
                  ? `var(--${bp}__${propVarName(key)}, ${str})`
                  : `var(--${bp}__${propVarName(key)})`,
                obj,
              ];
            },
            [bps[0] == '_' ? `${outputValue(key, styleProp[bps[0]])}` : '', sObj],
          );
          sObj[`--${propVarName(key)}`] = `${str}`;
        }
      } else {
        pObj[key] = inputProps[key];
      }
      return [sObj, cArr, pObj];
    },
    [style, classes, outputProps],
  );
  outputProps['style'] = style;
  outputProps['className'] = classes.length > 0 ? classes.join(' ') : undefined;
  return outputProps;
}

function Box({ as: Tag = 'div', ...props } = {}) {
  const boxProps = reduceStyleProps(props);
  return <Tag {...boxProps} />;
}

function Stack({ gap, gapY = gap, ...restArgs }) {
  const { style, className, ...restProps } = reduceStyleProps(restArgs);
  return <div className="stack"></div>;
}

export default function App() {
  return (
    <Box className="box" cols={{ _: 2, m: 4 }}>
      hello world
    </Box>
  );
}
