-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION json_model;" to load this file. \quit

-- DROP TYPE IF EXISTS jm_report_entry CASCADE;
-- DROP TABLE IF EXISTS jm_constant_maps;

CREATE TYPE jm_report_entry AS (message TEXT, path TEXT[]);
-- _jm_report_entry is jm_report_entry[]

--
-- constant maps for object tags
--
CREATE TABLE jm_constant_maps(
  mapname TEXT NOT NULL,
  tagval JSONB NOT NULL,
  value TEXT NOT NULL,
  PRIMARY KEY(mapname, tagval)
);

CREATE OR REPLACE FUNCTION
  jm_cmap_get(TEXT, JSONB)
RETURNS TEXT STRICT STABLE PARALLEL SAFE AS $$
  SELECT value
  FROM jm_constant_maps
  WHERE mapname = $1 AND tagval = $2;
$$ LANGUAGE sql;

--
-- dynamically call a function by name
--
CREATE OR REPLACE FUNCTION
  jm_call(fun TEXT, val JSONB, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
DECLARE
  res BOOLEAN;
BEGIN
  -- RAISE NOTICE 'jm_call: calling %', fun;
  EXECUTE FORMAT('SELECT %s($1, $2, $3)', fun)
    INTO res
    USING val, path, rep;
  RETURN res;
END;
$$ LANGUAGE plpgsql;

--
-- JSON Model predef validations
--
-- $DATE
CREATE OR REPLACE FUNCTION
  jm_is_valid_date(val TEXT, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
BEGIN
  IF val IS NULL THEN
    RETURN FALSE;
  END IF;
  PERFORM val::DATE;
  RETURN TRUE;
EXCEPTION
  WHEN data_exception THEN
    RETURN FALSE;
END;
$$ LANGUAGE plpgsql;

-- $DATETIME
CREATE OR REPLACE FUNCTION
  jm_is_valid_datetime(val TEXT, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
BEGIN
  IF val IS NULL THEN
    RETURN FALSE;
  END IF;
  PERFORM val::TIMESTAMP;
  RETURN TRUE;
EXCEPTION
  WHEN data_exception THEN
    RETURN FALSE;
END;
$$ LANGUAGE plpgsql;

-- $TIME
CREATE OR REPLACE FUNCTION
  jm_is_valid_time(val TEXT, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
BEGIN
  IF val IS NULL THEN
    RETURN FALSE;
  END IF;
  PERFORM val::TIME;
  RETURN TRUE;
EXCEPTION
  WHEN data_exception THEN
    RETURN FALSE;
END;
$$ LANGUAGE plpgsql;

-- $REGEX
CREATE OR REPLACE FUNCTION
  jm_is_valid_regex(val TEXT, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
BEGIN
  IF val IS NULL THEN
    RETURN FALSE;
  END IF;
  PERFORM '' ~ val;
  RETURN TRUE;
EXCEPTION
  WHEN data_exception THEN
    RETURN FALSE;
END;
$$ LANGUAGE plpgsql;

-- $EXTREG
CREATE OR REPLACE FUNCTION
  jm_is_valid_extreg(val TEXT, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
  SELECT jm_is_valid_regex(val, path, rep);
$$ LANGUAGE sql;

-- $UUID
CREATE OR REPLACE FUNCTION
  jm_is_valid_uuid(val TEXT, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
BEGIN
  IF val IS NULL THEN
    RETURN FALSE;
  END IF;
  PERFORM val::UUID;
  RETURN TRUE;
EXCEPTION
  WHEN data_exception THEN
    RETURN FALSE;
END;
$$ LANGUAGE plpgsql;

-- $URL (loose)
CREATE OR REPLACE FUNCTION
  jm_is_valid_url(val TEXT, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
BEGIN
  RETURN val IS NOT NULL AND val ~ '^((https?|file)://|\.)\S*$';
END;
$$ LANGUAGE plpgsql;

-- $EMAIL
CREATE OR REPLACE FUNCTION
  jm_is_valid_email(val TEXT, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
BEGIN
  RETURN val IS NOT NULL AND val ~ '^[_A-Za-z0-9_.]+@[_A-Za-z0-9_.]+$';
END;
$$ LANGUAGE plpgsql;

--
-- array uniqueness by sorting and adjacent item comparison
--
CREATE OR REPLACE FUNCTION
  jm_array_is_unique(val JSONB, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
DECLARE
  previous JSONB := NULL;
  rec RECORD;
BEGIN
  IF jsonb_typeof(val) != 'array' THEN
    RETURN FALSE;
  END IF;
  FOR rec IN
    SELECT jv FROM JSONB_ARRAY_ELEMENTS(val) AS jv ORDER BY 1
  LOOP
    IF previous IS NOT NULL AND previous = rec.jv THEN
      RETURN FALSE;
    END IF;
    previous := rec.jv;
  END LOOP;
  RETURN TRUE;
END;
$$ LANGUAGE plpgsql;

-- WTF
CREATE OR REPLACE FUNCTION
  jm_object_size(val JSONB)
RETURNS INT STRICT IMMUTABLE PARALLEL SAFE AS $$
  SELECT COUNT(*) FROM JSONB_EACH(val);
$$ LANGUAGE sql;

--
-- (slow) dynamic value to constant comparison
--
CREATE OR REPLACE FUNCTION
  jm_check_constraint(val JSONB, op TEXT, cst ANYELEMENT, path TEXT[], rep jm_report_entry[])
RETURNS BOOLEAN CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE AS $$
DECLARE
  cval JSONB := val;
  vt TEXT := JSONB_TYPEOF(val);
  ct TEXT := pg_typeof(cst);
  const JSONB;
BEGIN
  -- filter on types
  IF vt IN ('null', 'boolean') THEN
    RETURN FALSE;
  END IF;

  IF ct = 'unknown' THEN
    const := TO_JSONB(cst::TEXT);
  ELSEIF ct IN ('integer', 'numeric', 'double precision') THEN
    const := TO_JSONB(cst);
  ELSE
    RAISE EXCEPTION 'unexpected constant type %', ct;
  END IF;

  -- get an integer in some cases
  IF ct = 'integer' THEN
    IF vt = 'number' THEN
      NULL;
    ELSEIF vt = 'string' THEN
      cval := TO_JSONB(LENGTH(JSON_VALUE(val, '$' RETURNING TEXT)));
    ELSEIF vt = 'array' THEN
      cval := TO_JSONB(JSONB_ARRAY_LENGTH(val));
    ELSEIF vt = 'object' THEN
      cval := TO_JSONB(jm_object_size(val));
    ELSE
      RAISE NOTICE 'unexpected value type for integer comparison: %', vt;
    END IF;
  END IF;

  -- actual comparison, which may fail depending on types
  IF op = '=' THEN
    RETURN cval = const;
  ELSEIF op = '!=' THEN
    RETURN cval <> const;
  ELSEIF op = '<' THEN
    RETURN cval < const;
  ELSEIF op = '<=' THEN
    RETURN cval <= const;
  ELSEIF op = '>' THEN
    RETURN cval < const;
  ELSEIF op = '>=' THEN
    RETURN cval >= const;
  ELSE
    RAISE EXCEPTION 'unexpected comparison operator %', op;
  END IF;
END;
$$ LANGUAGE plpgsql;

--
-- FIXME REPORTING
--
-- clear current report
CREATE OR REPLACE PROCEDURE
  jm_report_free_entries(INOUT rep jm_report_entry[]) AS $$
BEGIN
  rep := ARRAY[];
END;
$$ LANGUAGE plpgsql;

-- TODO INOUT
CREATE OR REPLACE PROCEDURE
  jm_report_add_entry(INOUT rep jm_report_entry[], message TEXT, path TEXT[]) AS $$
BEGIN
  rep := array_append(rep, ROW(message, path));
END;
$$ LANGUAGE plpgsql;
