#include "allocate.h"
#include "dense.h"
#include "eliminate.h"
#include "inline.h"
#include "print.h"
#include "ternary.h"
#include "report.h"
#include "sort.h"
#include "terminate.h"

static void connect_ternary_clauses(kissat *solver, value *ternary)
{
    assert(!solver->level);
    assert(!solver->watching);
    size_t connected = 0;
    value *values = solver->values;
    for (all_clauses(c)) {
        if (c->garbage)
            continue;
        if (c->size != 3)
            continue;
        const unsigned *lits = c->lits;
        if (values[lits[0]])
            continue;
        if (values[lits[1]])
            continue;
        if (values[lits[2]])
            continue;
        ternary[lits[0]] = ternary[lits[1]] = ternary[lits[2]] = 1;
        kissat_connect_clause(solver, c);
        connected++;
    }
}

static unsigned schedule_ternary(kissat *solver, value *ternary)
{
    heap *schedule = &solver->schedule;
    flags *flags = solver->flags;

    assert(!schedule->size);
    unsigned scheduled = 0;

    for (all_variables(idx)) {
        struct flags *f = flags + idx;
        if (!f->active)
            continue;
        const unsigned lit = LIT(idx);
        if (!ternary[lit])
            continue;
        const unsigned not_lit = NOT(lit);
        if (!ternary[not_lit])
            continue;
        if (!schedule->size)
            kissat_resize_heap(solver, schedule, solver->vars);
        kissat_update_variable_score(solver, schedule, idx);
        kissat_push_heap(solver, schedule, idx);
        scheduled++;
    }

    if (!scheduled)
        return 0;

    kissat_phase(solver, "ternary", GET(hyper_ternary_phases), "scheduled %u variables %.0f%%", scheduled,
        kissat_percent(scheduled, solver->active));

    return scheduled;
}

typedef struct tag tag;

struct tag {
    unsigned first : 30;
    bool redundant : 1;
    bool binary : 1;
};

// *INDENT-OFF*
typedef STACK(tag)tags;
// *INDENT-ON*

static void ternary_resolution(kissat *solver, tags *tags, references *garbage, unsigned lit, clause *c, clause *d)
{
    assert(c->size == 3);
    assert(d->size == 3);

    LOGCLS(c, "ternary 1st antecedent");
    LOGCLS(d, "ternary 2nd antecedent");

    INC(hyper_ternary_resolutions);

    unsigned lits[4];
    unsigned size = 0;
    for (unsigned i = 0; i < 3; i++) {
        const unsigned other = c->lits[i];
        if (other == lit)
            continue;
        lits[size++] = other;
    }
    assert(size == 2);

    const unsigned not_lit = NOT(lit);
    bool tautological = false;
    for (unsigned i = 0; i < 3; i++) {
        const unsigned other = d->lits[i];
        if (other == not_lit)
            continue;
        const unsigned not_other = NOT(other);
        bool found = false;
        for (unsigned j = 0; j < size; j++) {
            const unsigned prev = lits[j];
            if (other == prev) {
                found = true;
                break;
            }
            if (not_other == prev) {
                tautological = true;
#ifndef LOGGING
                break;
#endif
            }
        }
#ifndef LOGGING
        if (tautological)
            break;
#endif
        if (found)
            continue;
        assert(size < 4);
        lits[size++] = other;
    }

    if (tautological) {
        LOGLITS(size, lits, "tautological resolvent");
        return;
    }

    if (size == 4) {
        LOGLITS(4, lits, "size 3 exceeding resolvent");
        return;
    }

    assert(size == 2 || size == 3);
    LOGLITS(size, lits, "ternary resolvent");

    for (unsigned i = size - 1; i; i--)
        PUSH_STACK(solver->delayed, lits[i]);

    bool redundant;
    if (size == 3)
        redundant = true;
    else {
        redundant = c->redundant && d->redundant;
        const reference c_ref = kissat_reference_clause(solver, c);
        const reference d_ref = kissat_reference_clause(solver, d);
        PUSH_STACK(*garbage, c_ref);
        PUSH_STACK(*garbage, d_ref);
        assert(!c->garbage), assert(!d->garbage);
        c->garbage = d->garbage = true;
    }

    tag tag;
    tag.first = lits[0];
    tag.redundant = redundant;
    tag.binary = (size == 2);

    PUSH_STACK(*tags, tag);
}

static bool find_binary(kissat *solver, bool irredundant, unsigned first, unsigned second)
{
    if (WATCHES(first).size > WATCHES(second).size)
        SWAP(unsigned, first, second);
    watches *watches = &WATCHES(first);
    const unsigned steps = kissat_cache_lines(watches->size, sizeof(watch));
    ADD(hyper_ternary_steps, steps + 1);
    for (all_binary_large_watches(watch, *watches)) {
        if (!watch.type.binary)
            continue;
        if (irredundant && watch.binary.redundant)
            continue;
        const unsigned other = watch.binary.lit;
        if (other == second)
            return true;
    }
    return false;
}

static bool find_ternary(kissat *solver, word *arena, unsigned first, unsigned second, unsigned third)
{
    if (WATCHES(second).size > WATCHES(third).size)
        SWAP(unsigned, second, third);
    if (WATCHES(first).size > WATCHES(second).size)
        SWAP(unsigned, first, second);
    watches *watches = &WATCHES(first);
    const unsigned steps = kissat_cache_lines(watches->size, sizeof(watch));
    ADD(hyper_ternary_steps, steps + 1);
    for (all_binary_large_watches(watch, *watches)) {
        if (watch.type.binary) {
            const unsigned other = watch.binary.lit;
            if (other == second)
                return true;
            if (other == third)
                return true;
        } else {
            const reference ref = watch.large.ref;
            clause *d = (clause *)(arena + ref);
            assert(kissat_clause_in_arena(solver, d));
            assert(d->size == 3);
            INC(hyper_ternary_steps);

            const unsigned a = d->lits[0];
            const unsigned b = d->lits[1];
            const unsigned c = d->lits[2];
            if (a == first) {
                if (b == second && c == third)
                    return true;
                if (c == second && b == third)
                    return true;
            } else if (b == first) {
                if (a == second && c == third)
                    return true;
                if (c == second && a == third)
                    return true;
            } else if (c == first) {
                if (a == second && b == third)
                    return true;
                if (b == second && a == third)
                    return true;
            }
        }
    }
    return find_binary(solver, false, second, third);
}

static void update_ternary_schedule_literal(kissat *solver, heap *schedule, bool reschedule, unsigned lit)
{
    const unsigned idx = IDX(lit);
    kissat_update_variable_score(solver, schedule, idx);
    if (reschedule && !kissat_heap_contains(schedule, idx))
        kissat_push_heap(solver, schedule, idx);
}

static void update_ternary_schedule_stack(kissat *solver, bool reschedule, unsigneds *stack)
{
    heap *schedule = &solver->schedule;
    for (all_stack(unsigned, lit, *stack))
        update_ternary_schedule_literal(solver, schedule, reschedule, lit);
}

static word *add_ternary_resolvents(kissat *solver, tags *tags, uint64_t *resolved_ptr)
{
    word *arena = BEGIN_STACK(solver->arena);
    while (!EMPTY_STACK(*tags)) {
        tag tag = POP_STACK(*tags);
        const bool binary = tag.binary;
        const bool redundant = tag.redundant;
        const unsigned first = tag.first;
        const unsigned second = POP_STACK(solver->delayed);
        const unsigned third = binary ? INVALID_LIT : POP_STACK(solver->delayed);
        if (binary) {
            if (find_binary(solver, !redundant, first, second))
                continue;
        } else {
            assert(third != INVALID_LIT);
            if (find_ternary(solver, arena, first, second, third))
                continue;
        }
        assert(EMPTY_STACK(solver->clause.lits));
        PUSH_STACK(solver->clause.lits, first);
        PUSH_STACK(solver->clause.lits, second);
        if (!binary)
            PUSH_STACK(solver->clause.lits, third);
        if (!redundant) {
            assert(binary);
            (void)kissat_new_irredundant_clause(solver);
        } else if (binary) {
            (void)kissat_new_binary_clause(solver, true, first, second);
            assert(arena == BEGIN_STACK(solver->arena));
        } else {
            reference ref = kissat_new_redundant_clause(solver, 2);
            clause *c = kissat_dereference_clause(solver, ref);
            assert(c->redundant);
            assert(c->size == 3);
            INC(hyper_ternaries);
            assert(!c->hyper);
            c->hyper = true;
            LOGCLS(c, "actually");
        }
        arena = BEGIN_STACK(solver->arena);

        if (binary)
            INC(hyper_ternary2_resolved);
        else
            INC(hyper_ternary3_resolved);
        INC(hyper_ternary_resolved);
        *resolved_ptr += 1;

        update_ternary_schedule_stack(solver, !binary, &solver->clause.lits);
        CLEAR_STACK(solver->clause.lits);
    }
    assert(EMPTY_STACK(solver->delayed));

    return arena;
}

static void remove_ternary_subsumed_clauses(kissat *solver, references *garbage)
{
    size_t marked = 0;
    for (all_stack(reference, ref, *garbage)) {
        clause *c = kissat_dereference_clause(solver, ref);
        assert(c->garbage);
        c->garbage = false;
        kissat_mark_clause_as_garbage(solver, c);
        marked++;
    }
    LOG("marked %zu clauses as garbage", marked);
    CLEAR_STACK(*garbage);
    (void)marked;
}

static word *resolve_ternary_clauses(kissat *solver, word *arena, uint64_t steps_limit, tags *tags, references *garbage,
    unsigned lit, uint64_t *resolved_ptr)
{
    assert(!solver->level);
    LOG("resolving ternary clauses on literal %s", LOGLIT(lit));
    const unsigned not_lit = NOT(lit);

    watches *pos_watches = &WATCHES(lit);
    watches *neg_watches = &WATCHES(not_lit);

    assert(EMPTY_STACK(*garbage));
    assert(EMPTY_STACK(solver->delayed));

    for (all_binary_large_watches(pos_watch, *pos_watches)) {
        if (TERMINATED(13))
            break;
        if (solver->statistics.hyper_ternary_steps > steps_limit)
            break;
        if (pos_watch.type.binary)
            continue;
        const reference pos_ref = pos_watch.large.ref;
        clause *c = (clause *)(arena + pos_ref);
        assert(kissat_clause_in_arena(solver, c));
        assert(c->size == 3);
        INC(hyper_ternary_steps);
        if (c->garbage)
            continue;
        for (all_binary_large_watches(neg_watch, *neg_watches)) {
            if (neg_watch.type.binary)
                continue;
            const reference neg_ref = neg_watch.large.ref;
            clause *d = (clause *)(arena + neg_ref);
            assert(kissat_clause_in_arena(solver, d));
            assert(d->size == 3);
            assert(c != d);
            INC(hyper_ternary_steps);
            if (d->garbage)
                continue;
            ternary_resolution(solver, tags, garbage, lit, c, d);
            if (c->garbage)
                break;
        }
    }

    word *res = add_ternary_resolvents(solver, tags, resolved_ptr);
    remove_ternary_subsumed_clauses(solver, garbage);

    return res;
}

static bool really_ternary(kissat *solver)
{
    if (!GET_OPTION(really))
        return true;

    const uint64_t limit = 2 * CLAUSES;
    statistics *statistics = &solver->statistics;

    if (limit >= statistics->search_ticks + GET_OPTION(ternarymineff))
        return false;

    for (all_clauses(c))
        if (!c->garbage && c->size == 3)
            return true;

    return false;
}

static uint64_t ternary_round(kissat *solver, const uint64_t resolved_limit, const uint64_t steps_limit)
{
    tags tags;
    INIT_STACK(tags);

    references garbage;
    INIT_STACK(garbage);

    word *arena = BEGIN_STACK(solver->arena);
    heap *schedule = &solver->schedule;

#ifndef QUIET
    size_t scheduled = kissat_size_heap(schedule);
#endif
    uint64_t resolved = 0;

    while (!kissat_empty_heap(schedule)) {
        if (TERMINATED(14))
            break;

        if (solver->statistics.hyper_ternary_steps > steps_limit) {
            kissat_phase(solver, "ternary", GET(hyper_ternary_phases), "steps limit %" PRIu64 " reached", steps_limit);
            break;
        }

        if (resolved > resolved_limit) {
            kissat_phase(solver, "ternary", GET(hyper_ternary_phases), "resolved limit %" PRIu64 " reached",
                resolved_limit);
            break;
        }

        const unsigned idx = kissat_max_heap(schedule);
        kissat_pop_heap(solver, schedule, idx);
        assert(ACTIVE(idx));

        const unsigned lit = LIT(idx);
        arena = resolve_ternary_clauses(solver, arena, steps_limit, &tags, &garbage, lit, &resolved);
    }

#ifndef QUIET
    size_t remain = kissat_size_heap(schedule);
    if (remain)
        kissat_phase(solver, "ternary", GET(hyper_ternary_phases),
            "remaining %u variables %.0f%% (incomplete ternary round)", remain, kissat_percent(remain, scheduled));
    else
        kissat_phase(solver, "ternary", GET(hyper_ternary_phases), "all %u variables resolved (complete ternary round)",
            scheduled);
#endif

    RELEASE_STACK(tags);
    RELEASE_STACK(garbage);

    return resolved;
}

void kissat_ternary(kissat *solver)
{
    if (solver->inconsistent)
        return;

    assert(!solver->level);
    assert(solver->watching);
    assert(solver->probing);

    if (TERMINATED(15))
        return;
    if (!GET_OPTION(ternary))
        return;

    if (!really_ternary(solver))
        return;

    RETURN_IF_DELAYED(ternary);

    START(ternary);
    INC(hyper_ternary_phases);

#ifndef NMETRICS
    uint64_t resolved2 = solver->statistics.hyper_ternary2_resolved;
    uint64_t resolved3 = solver->statistics.hyper_ternary3_resolved;
#endif

    kissat_enter_dense_mode(solver, 0, 0);

    value *ternary = kissat_calloc(solver, LITS, sizeof *ternary);
    connect_ternary_clauses(solver, ternary);

    const double resolved_limit_fraction = GET_OPTION(ternarymaxadd) * 0.01;
    const uint64_t resolved_limit = CLAUSES * resolved_limit_fraction;

    const unsigned scheduled = schedule_ternary(solver, ternary);
    uint64_t resolved = 0;

    if (scheduled) {
        SET_EFFICIENCY_BOUND(steps_limit, ternary, hyper_ternary_steps, search_ticks,
            2 * CLAUSES + kissat_nlogn(scheduled));

        resolved = ternary_round(solver, resolved_limit, steps_limit);
        kissat_release_heap(solver, &solver->schedule);
    }

    kissat_dealloc(solver, ternary, LITS, sizeof *ternary);
    kissat_resume_sparse_mode(solver, false, 0, 0);

#ifndef NMETRICS
    resolved2 = solver->statistics.hyper_ternary2_resolved - resolved2;
    resolved3 = solver->statistics.hyper_ternary3_resolved - resolved3;
    kissat_phase(solver, "ternary", GET(hyper_ternary_phases),
        "resolved %" PRIu64 " clauses (%" PRIu64 " ternary %.0f%% and %" PRIu64 " binary %.0f%%)", resolved, resolved3,
        kissat_percent(resolved3, resolved), resolved2, kissat_percent(resolved2, resolved));
#else
    kissat_phase(solver, "ternary", GET(hyper_ternary_phases), "resolved %" PRIu64 " clauses", resolved);
#endif
    UPDATE_DELAY(resolved, ternary);
    REPORT(!resolved, '3');
    kissat_check_statistics(solver);
    STOP(ternary);
}
