CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$

-- version: 4
DECLARE
  changes jsonb;
  version jsonb;
  full_snapshot boolean;
  log_data jsonb;
  new_v integer;
  size integer;
  history_limit integer;
  debounce_time integer;
  current_version integer;
  k text;
  iterator integer;
  item record;
  columns text[];
  include_columns boolean;
  ts timestamp with time zone;
  ts_column text;
  err_sqlstate text;
  err_message text;
  err_detail text;
  err_hint text;
  err_context text;
  err_table_name text;
  err_schema_name text;
  err_jsonb jsonb;
  err_captured boolean;
BEGIN
  ts_column := NULLIF(TG_ARGV[1], 'null');
  columns := NULLIF(TG_ARGV[2], 'null');
  include_columns := NULLIF(TG_ARGV[3], 'null');

  IF NEW.log_data is NULL OR NEW.log_data = '{}'::jsonb
  THEN
    IF columns IS NOT NULL THEN
      log_data = logidze_snapshot(to_jsonb(NEW.*), ts_column, columns, include_columns);
    ELSE
      log_data = logidze_snapshot(to_jsonb(NEW.*), ts_column);
    END IF;

    IF log_data#>>'{h, -1, c}' != '{}' THEN
      NEW.log_data := log_data;
    END IF;

  ELSE

    IF TG_OP = 'UPDATE' AND (to_jsonb(NEW.*) = to_jsonb(OLD.*)) THEN
      RETURN NEW; -- pass
    END IF;

    history_limit := NULLIF(TG_ARGV[0], 'null');
    debounce_time := NULLIF(TG_ARGV[4], 'null');

    log_data := NEW.log_data;

    current_version := (log_data->>'v')::int;

    IF ts_column IS NULL THEN
      ts := statement_timestamp();
    ELSEIF TG_OP = 'UPDATE' THEN
      ts := (to_jsonb(NEW.*) ->> ts_column)::timestamp with time zone;
      IF ts IS NULL OR ts = (to_jsonb(OLD.*) ->> ts_column)::timestamp with time zone THEN
        ts := statement_timestamp();
      END IF;
    ELSEIF TG_OP = 'INSERT' THEN
      ts := (to_jsonb(NEW.*) ->> ts_column)::timestamp with time zone;
      IF ts IS NULL OR (extract(epoch from ts) * 1000)::bigint = (NEW.log_data #>> '{h,-1,ts}')::bigint THEN
        ts := statement_timestamp();
      END IF;
    END IF;

    full_snapshot := (coalesce(current_setting('logidze.full_snapshot', true), '') = 'on') OR (TG_OP = 'INSERT');

    IF current_version < (log_data#>>'{h,-1,v}')::int THEN
      iterator := 0;
      FOR item in SELECT * FROM jsonb_array_elements(log_data->'h')
      LOOP
        IF (item.value->>'v')::int > current_version THEN
          log_data := jsonb_set(
            log_data,
            '{h}',
            (log_data->'h') - iterator
          );
        END IF;
        iterator := iterator + 1;
      END LOOP;
    END IF;

    changes := '{}';

    IF full_snapshot THEN
      BEGIN
        changes = hstore_to_jsonb_loose(hstore(NEW.*));
      EXCEPTION
        WHEN NUMERIC_VALUE_OUT_OF_RANGE THEN
          changes = row_to_json(NEW.*)::jsonb;
          FOR k IN (SELECT key FROM jsonb_each(changes))
          LOOP
            IF jsonb_typeof(changes->k) = 'object' THEN
              changes = jsonb_set(changes, ARRAY[k], to_jsonb(changes->>k));
            END IF;
          END LOOP;
      END;
    ELSE
      BEGIN
        changes = hstore_to_jsonb_loose(
              hstore(NEW.*) - hstore(OLD.*)
          );
      EXCEPTION
        WHEN NUMERIC_VALUE_OUT_OF_RANGE THEN
          changes = (SELECT
            COALESCE(json_object_agg(key, value), '{}')::jsonb
            FROM
            jsonb_each(row_to_json(NEW.*)::jsonb)
            WHERE NOT jsonb_build_object(key, value) <@ row_to_json(OLD.*)::jsonb);
          FOR k IN (SELECT key FROM jsonb_each(changes))
          LOOP
            IF jsonb_typeof(changes->k) = 'object' THEN
              changes = jsonb_set(changes, ARRAY[k], to_jsonb(changes->>k));
            END IF;
          END LOOP;
      END;
    END IF;

    changes = changes - 'log_data';

    IF columns IS NOT NULL THEN
      changes = logidze_filter_keys(changes, columns, include_columns);
    END IF;

    IF changes = '{}' THEN
      RETURN NEW; -- pass
    END IF;

    new_v := (log_data#>>'{h,-1,v}')::int + 1;

    size := jsonb_array_length(log_data->'h');
    version := logidze_version(new_v, changes, ts);

    IF (
      debounce_time IS NOT NULL AND
      (version->>'ts')::bigint - (log_data#>'{h,-1,ts}')::text::bigint <= debounce_time
    ) THEN
      -- merge new version with the previous one
      new_v := (log_data#>>'{h,-1,v}')::int;
      version := logidze_version(new_v, (log_data#>'{h,-1,c}')::jsonb || changes, ts);
      -- remove the previous version from log
      log_data := jsonb_set(
        log_data,
        '{h}',
        (log_data->'h') - (size - 1)
      );
    END IF;

    log_data := jsonb_set(
      log_data,
      ARRAY['h', size::text],
      version,
      true
    );

    log_data := jsonb_set(
      log_data,
      '{v}',
      to_jsonb(new_v)
    );

    IF history_limit IS NOT NULL AND history_limit <= size THEN
      log_data := logidze_compact_history(log_data, size - history_limit + 1);
    END IF;

    NEW.log_data := log_data;
  END IF;

  RETURN NEW; -- result
EXCEPTION
  WHEN OTHERS THEN
    GET STACKED DIAGNOSTICS err_sqlstate = RETURNED_SQLSTATE,
                            err_message = MESSAGE_TEXT,
                            err_detail = PG_EXCEPTION_DETAIL,
                            err_hint = PG_EXCEPTION_HINT,
                            err_context = PG_EXCEPTION_CONTEXT,
                            err_schema_name = SCHEMA_NAME,
                            err_table_name = TABLE_NAME;
    err_jsonb := jsonb_build_object(
      'returned_sqlstate', err_sqlstate,
      'message_text', err_message,
      'pg_exception_detail', err_detail,
      'pg_exception_hint', err_hint,
      'pg_exception_context', err_context,
      'schema_name', err_schema_name,
      'table_name', err_table_name
    );
    err_captured = logidze_capture_exception(err_jsonb);
    IF err_captured THEN
      return NEW;
    ELSE
      RAISE;
    END IF;
END;

$body$ LANGUAGE plpgsql;