//-----------------------------------------------------------------------------
// Generate our model based on its parametric description, by solving each
// sketch, generating surfaces from the resulting entities, performing any
// requested surface operations (e.g. Booleans) with our model so far, and
// then repeating this process for each subsequent group.
//
// Copyright 2008-2013 Jonathan Westhues.
//-----------------------------------------------------------------------------
#include "solvespace.h"

void SolveSpaceUI::MarkGroupDirtyByEntity(hEntity he) {
    Entity *e = SK.GetEntity(he);
    MarkGroupDirty(e->group);
}

void SolveSpaceUI::MarkGroupDirty(hGroup hg, bool onlyThis) {
    bool go = false;
    for(auto const &gh : SK.groupOrder) {
        Group *g = SK.GetGroup(gh);
        if(g->h == hg) {
            go = true;
        }
        if(go) {
            g->clean = false;
            if(onlyThis) break;
        }
    }
    unsaved = true;
    ScheduleGenerateAll();
}

bool SolveSpaceUI::PruneOrphans() {

    auto r = std::find_if(SK.request.begin(), SK.request.end(),
                          [&](Request &r) { return !GroupExists(r.group); });
    if(r != SK.request.end()) {
        (deleted.requests)++;
        SK.request.RemoveById(r->h);
        return true;
    }

    auto c = std::find_if(SK.constraint.begin(), SK.constraint.end(),
                          [&](Constraint &c) { return !GroupExists(c.group); });
    if(c != SK.constraint.end()) {
        (deleted.constraints)++;
        (deleted.nonTrivialConstraints)++;
        SK.constraint.RemoveById(c->h);
        return true;
    }
    return false;
}

bool SolveSpaceUI::GroupsInOrder(hGroup before, hGroup after) {
    if(before.v == 0) return true;
    if(after.v  == 0) return true;
    if(!GroupExists(before)) return false;
    if(!GroupExists(after)) return false;
    int beforep = SK.GetGroup(before)->order;
    int afterp = SK.GetGroup(after)->order;
    if(beforep >= afterp) return false;
    return true;
}

bool SolveSpaceUI::GroupExists(hGroup hg) {
    // A nonexistent group is not acceptable
    return SK.group.FindByIdNoOops(hg) ? true : false;
}
bool SolveSpaceUI::EntityExists(hEntity he) {
    // A nonexstient entity is acceptable, though, usually just means it
    // doesn't apply.
    if(he == Entity::NO_ENTITY) return true;
    return SK.entity.FindByIdNoOops(he) ? true : false;
}

bool SolveSpaceUI::PruneGroups(hGroup hg) {
    Group *g = SK.GetGroup(hg);
    if(GroupsInOrder(g->opA, hg) &&
       EntityExists(g->predef.origin) &&
       EntityExists(g->predef.entityB) &&
       EntityExists(g->predef.entityC))
    {
        return false;
    }
    (deleted.groups)++;
    SK.group.RemoveById(g->h);
    return true;
}

bool SolveSpaceUI::PruneRequests(hGroup hg) {
    auto e = std::find_if(SK.entity.begin(), SK.entity.end(),
                          [&](Entity &e) { return e.group == hg && !EntityExists(e.workplane); });
    if(e != SK.entity.end()) {
        (deleted.requests)++;
        SK.entity.RemoveById(e->h);
        return true;
    }
    return false;
}

bool SolveSpaceUI::PruneConstraints(hGroup hg) {
    auto c = std::find_if(SK.constraint.begin(), SK.constraint.end(), [&](Constraint &c) {
        if(c.group != hg)
            return false;

        if(EntityExists(c.workplane) &&
           EntityExists(c.ptA) &&
           EntityExists(c.ptB) &&
           EntityExists(c.entityA) &&
           EntityExists(c.entityB) &&
           EntityExists(c.entityC) &&
           EntityExists(c.entityD)) {
            return false;
        }
        return true;
    });

    if(c != SK.constraint.end()) {
        (deleted.constraints)++;
        if(c->type != Constraint::Type::POINTS_COINCIDENT &&
           c->type != Constraint::Type::HORIZONTAL &&
           c->type != Constraint::Type::VERTICAL) {
            (deleted.nonTrivialConstraints)++;
        }

        SK.constraint.RemoveById(c->h);
        return true;
    }
    return false;
}

void SolveSpaceUI::GenerateAll(Generate type, bool andFindFree, bool genForBBox) {
    int first = 0, last = 0, i;

    uint64_t startMillis = GetMilliseconds(),
             endMillis;

    SK.groupOrder.Clear();
    for(auto &g : SK.group) { SK.groupOrder.Add(&g.h); }
    std::sort(SK.groupOrder.begin(), SK.groupOrder.end(),
        [](const hGroup &ha, const hGroup &hb) {
            return SK.GetGroup(ha)->order < SK.GetGroup(hb)->order;
        });

    switch(type) {
        case Generate::DIRTY: {
            first = INT_MAX;
            last  = 0;

            // Start from the first dirty group, and solve until the active group,
            // since all groups after the active group are hidden.
            // Not using range-for because we're tracking the indices.
            for(i = 0; i < SK.groupOrder.n; i++) {
                Group *g = SK.GetGroup(SK.groupOrder[i]);
                if((!g->clean) || !g->IsSolvedOkay()) {
                    first = min(first, i);
                }
                if(g->h == SS.GW.activeGroup) {
                    last = i;
                }
            }
            if(first == INT_MAX || last == 0) {
                // All clean; so just regenerate the entities, and don't solve anything.
                first = -1;
                last  = -1;
            } else {
                SS.nakedEdges.Clear();
            }
            break;
        }

        case Generate::ALL:
            first = 0;
            last  = INT_MAX;
            break;

        case Generate::REGEN:
            first = -1;
            last  = -1;
            break;

        case Generate::UNTIL_ACTIVE: {
            for(i = 0; i < SK.groupOrder.n; i++) {
                if(SK.groupOrder[i] == SS.GW.activeGroup)
                    break;
            }

            first = 0;
            last  = i;
            break;
        }
    }

    // If we're generating entities for display, first we need to find
    // the bounding box to turn relative chord tolerance to absolute.
    if(!SS.exportMode && !genForBBox) {
        GenerateAll(type, andFindFree, /*genForBBox=*/true);
        BBox box = SK.CalculateEntityBBox(/*includeInvisibles=*/true);
        Vector size = box.maxp.Minus(box.minp);
        double maxSize = std::max({ size.x, size.y, size.z });
        chordTolCalculated = maxSize * chordTol / 100.0;
    }

    // Remove any requests or constraints that refer to a nonexistent
    // group; can check those immediately, since we know what the list
    // of groups should be.
    while(PruneOrphans())
        ;

    // Don't lose our numerical guesses when we regenerate.
    IdList<Param,hParam> prev = {};
    SK.param.MoveSelfInto(&prev);
    SK.param.ReserveMore(prev.n);
    int oldEntityCount = SK.entity.n;
    SK.entity.Clear();
    SK.entity.ReserveMore(oldEntityCount);

    // Not using range-for because we're using the index inside the loop.
    for(i = 0; i < SK.groupOrder.n; i++) {
        hGroup hg = SK.groupOrder[i];

        // The group may depend on entities or other groups, to define its
        // workplane geometry or for its operands. Those must already exist
        // in a previous group, so check them before generating.
        if(PruneGroups(hg))
            goto pruned;

        int groupRequestIndex = 0;
        for(auto &req : SK.request) {
            Request *r = &req;
            if(r->group != hg) continue;
            r->groupRequestIndex = groupRequestIndex++;

            r->Generate(&(SK.entity), &(SK.param));
        }
        for(auto &con : SK.constraint) {
            Constraint *c = &con;
            if(c->group != hg) continue;

            c->Generate(&(SK.param));
        }
        SK.GetGroup(hg)->Generate(&(SK.entity), &(SK.param));

        // The requests and constraints depend on stuff in this or the
        // previous group, so check them after generating.
        if(PruneRequests(hg) || PruneConstraints(hg))
            goto pruned;

        // Use the previous values for params that we've seen before, as
        // initial guesses for the solver.
        for(auto &p : SK.param) {
            Param *newp = &p;
            if(newp->known) continue;

            Param *prevp = prev.FindByIdNoOops(newp->h);
            if(prevp) {
                newp->val = prevp->val;
                newp->free = prevp->free;
            }
        }

        if(hg == Group::HGROUP_REFERENCES) {
            ForceReferences();
            Group *g = SK.GetGroup(hg);
            g->solved.how = SolveResult::OKAY;
            g->clean = true;
        } else {
            // this i is an index in groupOrder
            if(i >= first && i <= last) {
                // The group falls inside the range, so really solve it,
                // and then regenerate the mesh based on the solved stuff.
                Group *g = SK.GetGroup(hg);
                if(genForBBox) {
                    SolveGroupAndReport(hg, andFindFree);
                    g->GenerateLoops();
                } else {
                    g->GenerateShellAndMesh();
                    g->clean = true;
                }
            } else {
                // The group falls outside the range, so just assume that
                // it's good wherever we left it. The mesh is unchanged,
                // and the parameters must be marked as known.
                for(auto &p : SK.param) {
                    Param *newp = &p;

                    Param *prevp = prev.FindByIdNoOops(newp->h);
                    if(prevp) newp->known = true;
                }
            }
        }
    }

    // And update any reference dimensions with their new values
    for(auto &con : SK.constraint) {
        Constraint *c = &con;
        if(c->reference) {
            c->ModifyToSatisfy();
        }
    }

    // Make sure the point that we're tracing exists.
    if(traced.point.v && !SK.entity.FindByIdNoOops(traced.point)) {
        traced.point = Entity::NO_ENTITY;
    }
    // And if we're tracing a point, add its new value to the path
    if(traced.point.v) {
        Entity *pt = SK.GetEntity(traced.point);
        traced.path.AddPoint(pt->PointGetNum());
    }

    prev.Clear();
    GW.Invalidate();

    // Remove nonexistent selection items, for same reason we waited till
    // the end to put up a dialog box.
    GW.ClearNonexistentSelectionItems();

    if(deleted.requests > 0 || deleted.constraints > 0 || deleted.groups > 0) {
        // All sorts of interesting things could have happened; for example,
        // the active group or active workplane could have been deleted. So
        // clear all that out.
        if(deleted.groups > 0) {
            SS.TW.ClearSuper();
        }
        ScheduleShowTW();
        GW.ClearSuper();

        // People get annoyed if I complain whenever they delete any request,
        // and I otherwise will, since those always come with pt-coincident
        // constraints.
        if(deleted.requests > 0 || deleted.nonTrivialConstraints > 0 ||
           deleted.groups > 0)
        {
            // Don't display any errors until we've regenerated fully. The
            // sketch is not necessarily in a consistent state until we've
            // pruned any orphaned etc. objects, and the message loop for the
            // messagebox could allow us to repaint and crash. But now we must
            // be fine.
            Message("Additional sketch elements were deleted, because they "
                    "depend on the element that was just deleted explicitly. "
                    "These include: \n"
                    "     %d request%s\n"
                    "     %d constraint%s\n"
                    "     %d group%s"
                    "%s",
                       deleted.requests, deleted.requests == 1 ? "" : "s",
                       deleted.constraints, deleted.constraints == 1 ? "" : "s",
                       deleted.groups, deleted.groups == 1 ? "" : "s",
                       undo.cnt > 0 ? "\n\nChoose Edit -> Undo to undelete all elements." : "");
        }

        deleted = {};
    }

    FreeAllTemporary();
    allConsistent = true;
    SS.GW.persistentDirty = true;
    SS.centerOfMass.dirty = true;

    endMillis = GetMilliseconds();

    if(endMillis - startMillis > 30) {
        const char *typeStr = "";
        switch(type) {
            case Generate::DIRTY:           typeStr = "DIRTY";        break;
            case Generate::ALL:             typeStr = "ALL";          break;
            case Generate::REGEN:           typeStr = "REGEN";        break;
            case Generate::UNTIL_ACTIVE:    typeStr = "UNTIL_ACTIVE"; break;
        }
        if(endMillis)
        dbp("Generate::%s%s took %lld ms",
            typeStr,
            (genForBBox ? " (for bounding box)" : ""),
            GetMilliseconds() - startMillis);
    }

    return;

pruned:
    // Restore the numerical guesses
    SK.param.Clear();
    prev.MoveSelfInto(&(SK.param));
    // Try again
    GenerateAll(type, andFindFree, genForBBox);
}

void SolveSpaceUI::ForceReferences() {
    // Force the values of the parameters that define the three reference
    // coordinate systems.
    static const struct {
        hRequest    hr;
        Quaternion  q;
    } Quat[] = {
        { Request::HREQUEST_REFERENCE_XY, { 1,    0,    0,    0,   } },
        { Request::HREQUEST_REFERENCE_YZ, { 0.5,  0.5,  0.5,  0.5, } },
        { Request::HREQUEST_REFERENCE_ZX, { 0.5, -0.5, -0.5, -0.5, } },
    };
    for(int i = 0; i < 3; i++) {
        hRequest hr = Quat[i].hr;
        Entity *wrkpl = SK.GetEntity(hr.entity(0));
        // The origin for our coordinate system, always zero
        Entity *origin = SK.GetEntity(wrkpl->point[0]);
        origin->PointForceTo(Vector::From(0, 0, 0));
        origin->construction = true;
        SK.GetParam(origin->param[0])->known = true;
        SK.GetParam(origin->param[1])->known = true;
        SK.GetParam(origin->param[2])->known = true;
        // The quaternion that defines the rotation, from the table.
        Entity *normal = SK.GetEntity(wrkpl->normal);
        normal->NormalForceTo(Quat[i].q);
        SK.GetParam(normal->param[0])->known = true;
        SK.GetParam(normal->param[1])->known = true;
        SK.GetParam(normal->param[2])->known = true;
        SK.GetParam(normal->param[3])->known = true;
    }
}

void SolveSpaceUI::UpdateCenterOfMass() {
    SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
    SS.centerOfMass.position = m->GetCenterOfMass();
    SS.centerOfMass.dirty = false;
}

void SolveSpaceUI::MarkDraggedParams() {
    sys.dragged.Clear();

    for(int i = -1; i < SS.GW.pending.points.n; i++) {
        hEntity hp;
        if(i == -1) {
            hp = SS.GW.pending.point;
        } else {
            hp = SS.GW.pending.points[i];
        }
        if(!hp.v) continue;

        // The pending point could be one in a group that has not yet
        // been processed, in which case the lookup will fail; but
        // that's not an error.
        Entity *pt = SK.entity.FindByIdNoOops(hp);
        if(pt) {
            switch(pt->type) {
                case Entity::Type::POINT_N_TRANS:
                case Entity::Type::POINT_IN_3D:
                case Entity::Type::POINT_N_ROT_AXIS_TRANS:
                    sys.dragged.Add(&(pt->param[0]));
                    sys.dragged.Add(&(pt->param[1]));
                    sys.dragged.Add(&(pt->param[2]));
                    break;

                case Entity::Type::POINT_IN_2D:
                    sys.dragged.Add(&(pt->param[0]));
                    sys.dragged.Add(&(pt->param[1]));
                    break;

                default: // Only the entities above can be dragged.
                    break;
            }
        }
    }
    if(SS.GW.pending.circle.v) {
        Entity *circ = SK.entity.FindByIdNoOops(SS.GW.pending.circle);
        if(circ) {
            Entity *dist = SK.GetEntity(circ->distance);
            switch(dist->type) {
                case Entity::Type::DISTANCE:
                    sys.dragged.Add(&(dist->param[0]));
                    break;

                default: // Only the entities above can be dragged.
                    break;
            }
        }
    }
    if(SS.GW.pending.normal.v) {
        Entity *norm = SK.entity.FindByIdNoOops(SS.GW.pending.normal);
        if(norm) {
            switch(norm->type) {
                case Entity::Type::NORMAL_IN_3D:
                    sys.dragged.Add(&(norm->param[0]));
                    sys.dragged.Add(&(norm->param[1]));
                    sys.dragged.Add(&(norm->param[2]));
                    sys.dragged.Add(&(norm->param[3]));
                    break;

                default: // Only the entities above can be dragged.
                    break;
            }
        }
    }
}

void SolveSpaceUI::SolveGroupAndReport(hGroup hg, bool andFindFree) {
    SolveGroup(hg, andFindFree);

    Group *g = SK.GetGroup(hg);
    bool isOkay = g->solved.how == SolveResult::OKAY ||
                  (g->allowRedundant && g->solved.how == SolveResult::REDUNDANT_OKAY);
    if(!isOkay || (isOkay && !g->IsSolvedOkay())) {
        TextWindow::ReportHowGroupSolved(g->h);
    }
}

void SolveSpaceUI::WriteEqSystemForGroup(hGroup hg) {
    // Clear out the system to be solved.
    sys.entity.Clear();
    sys.param.Clear();
    sys.eq.Clear();
    // And generate all the params for requests in this group
    for(auto &req : SK.request) {
        Request *r = &req;
        if(r->group != hg) continue;

        r->Generate(&(sys.entity), &(sys.param));
    }
    for(auto &con : SK.constraint) {
        Constraint *c = &con;
        if(c->group != hg) continue;

        c->Generate(&(sys.param));
    }
    // And for the group itself
    Group *g = SK.GetGroup(hg);
    g->Generate(&(sys.entity), &(sys.param));
    // Set the initial guesses for all the params
    for(auto &param : sys.param) {
        Param *p = &param;
        p->known = false;
        p->val = SK.GetParam(p->h)->val;
    }

    MarkDraggedParams();
}

void SolveSpaceUI::SolveGroup(hGroup hg, bool andFindFree) {
    WriteEqSystemForGroup(hg);
    Group *g = SK.GetGroup(hg);
    g->solved.remove.Clear();
    g->solved.findToFixTimeout = SS.timeoutRedundantConstr;
    SolveResult how = sys.Solve(g, NULL,
                                   &(g->solved.dof),
                                   &(g->solved.remove),
                                   /*andFindBad=*/!g->allowRedundant,
                                   /*andFindFree=*/andFindFree,
                                   /*forceDofCheck=*/!g->dofCheckOk);
    if(how == SolveResult::OKAY) {
        g->dofCheckOk = true;
    }
    g->solved.how = how;
    FreeAllTemporary();
}

SolveResult SolveSpaceUI::TestRankForGroup(hGroup hg, int *rank) {
    Group *g = SK.GetGroup(hg);
    // If we don't calculate dof or redundant is allowed, there is
    // no point to solve rank because this result is not meaningful
    if(g->suppressDofCalculation || g->allowRedundant) return SolveResult::OKAY;
    WriteEqSystemForGroup(hg);
    SolveResult result = sys.SolveRank(g, rank);
    FreeAllTemporary();
    return result;
}

bool SolveSpaceUI::ActiveGroupsOkay() {
    for(int i = 0; i < SK.groupOrder.n; i++) {
        Group *g = SK.GetGroup(SK.groupOrder[i]);
        if(!g->IsSolvedOkay())
            return false;
        if(g->h == SS.GW.activeGroup)
            break;
    }
    return true;
}

