#include <common/mapfile.hh>
#include <common/log.hh>
#include <common/ostream.hh>
#include <common/imglib.hh>
#include <utility>

namespace mapfile
{

std::ostream &operator<<(std::ostream &os, const texdef_quake_ed_t &v)
{
    os << fmt::format("{{shift: {}, rotate: {}, scale: {}}}", v.shift, v.rotate, v.scale);
    return os;
}

std::ostream &operator<<(std::ostream &os, const texdef_valve_t &v)
{
    os << fmt::format("{{shift: {}, rotate: {}, scale: {}, axis.row(0): {}, axis.row(1): {}}}", v.shift, v.rotate,
        v.scale, v.axis.row(0), v.axis.row(1));
    return os;
}

/*static*/ bool brush_side_t::is_valid_texture_projection(const qvec3f &faceNormal, const texvecf &vecs)
{
    // TODO: This doesn't match how light does it (TexSpaceToWorld)

    for (int row = 0; row < 2; ++row) {
        for (int col = 0; col < 4; ++col) {
            if (std::isnan(vecs.row(row)[col])) {
                return false;
            }
        }
    }

    qvec3f s_vec = vecs.row(0).xyz();
    qvec3f t_vec = vecs.row(1).xyz();

    const qvec3f tex_normal = qv::normalize(qv::cross(s_vec, t_vec));

    for (size_t i = 0; i < 3; i++) {
        if (std::isnan(tex_normal[i])) {
            return false;
        }
    }

    const float cosangle = qv::dot(tex_normal, faceNormal);

    if (std::isnan(cosangle)) {
        return false;
    } else if (fabs(cosangle) < ZERO_EPSILON) {
        return false;
    }

    return true;
}

bool brush_side_t::is_valid_texture_projection() const
{
    return is_valid_texture_projection(plane.normal, vecs);
}

void brush_side_t::validate_texture_projection()
{
    if (!is_valid_texture_projection()) {
        logging::print("WARNING: {}: repairing invalid texture projection (\"{}\" near {} {} {})\n", location, texture,
            (int)planepts[0][0], (int)planepts[0][1], (int)planepts[0][2]);

        // Reset texturing to sensible defaults
        set_texinfo(texdef_quake_ed_t{{0.0, 0.0}, 0, {1.0, 1.0}});

        Q_assert(is_valid_texture_projection());
    }
}

/*static*/ texdef_bp_t brush_side_t::parse_bp(parser_t &parser)
{
    qmat<double, 2, 3> texMat;

    parser.parse_token(PARSE_SAMELINE);

    if (parser.token != "(") {
        goto parse_error;
    }

    for (size_t i = 0; i < 2; i++) {
        parser.parse_token(PARSE_SAMELINE);
        if (parser.token != "(") {
            goto parse_error;
        }

        for (size_t j = 0; j < 3; j++) {
            parser.parse_token(PARSE_SAMELINE);
            texMat.at(i, j) = std::stod(parser.token);
        }

        parser.parse_token(PARSE_SAMELINE);

        if (parser.token != ")") {
            goto parse_error;
        }
    }

    parser.parse_token(PARSE_SAMELINE);

    if (parser.token != ")") {
        goto parse_error;
    }

    return {texMat};

parse_error:
    FError("{}: couldn't parse Brush Primitives texture info", parser.location);
}

/*static*/ texdef_valve_t brush_side_t::parse_valve_220(parser_t &parser)
{
    qmat<double, 2, 3> axis;
    qvec2d shift, scale;
    double rotate;

    for (size_t i = 0; i < 2; i++) {
        parser.parse_token(PARSE_SAMELINE);

        if (parser.token != "[") {
            goto parse_error;
        }

        for (size_t j = 0; j < 3; j++) {
            parser.parse_token(PARSE_SAMELINE);
            axis.at(i, j) = std::stod(parser.token);
        }

        parser.parse_token(PARSE_SAMELINE);
        shift[i] = std::stod(parser.token);
        parser.parse_token(PARSE_SAMELINE);

        if (parser.token != "]") {
            goto parse_error;
        }
    }
    parser.parse_token(PARSE_SAMELINE);
    rotate = std::stod(parser.token);
    parser.parse_token(PARSE_SAMELINE);
    scale[0] = std::stod(parser.token);
    parser.parse_token(PARSE_SAMELINE);
    scale[1] = std::stod(parser.token);

    return {shift, rotate, scale, axis};

parse_error:
    FError("{}: couldn't parse Valve220 texture info", parser.location);
}

/*static*/ texdef_quake_ed_t brush_side_t::parse_quake_ed(parser_t &parser)
{
    qvec2d shift, scale;
    double rotate;

    parser.parse_token(PARSE_SAMELINE);
    shift[0] = std::stod(parser.token);
    parser.parse_token(PARSE_SAMELINE);
    shift[1] = std::stod(parser.token);

    parser.parse_token(PARSE_SAMELINE);
    rotate = std::stod(parser.token);

    parser.parse_token(PARSE_SAMELINE);
    scale[0] = std::stod(parser.token);
    parser.parse_token(PARSE_SAMELINE);
    scale[1] = std::stod(parser.token);

    return {shift, rotate, scale};
}

bool brush_side_t::parse_quark_comment(parser_t &parser)
{
    if (!parser.parse_token(PARSE_COMMENT | PARSE_OPTIONAL)) {
        return false;
    }

    if (parser.token.length() < 5 || strncmp(parser.token.c_str(), "//TX", 4)) {
        return false;
    }

    // QuArK TX modes can only exist on Quaked-style maps
    Q_assert(std::holds_alternative<texdef_quake_ed_t>(raw));

    if (parser.token[4] != '1' && parser.token[4] != '2') {
        return false;
    }

    raw = texdef_etp_t{std::get<texdef_quake_ed_t>(raw), parser.token[4] == '2'};
    return true;
}

void brush_side_t::parse_extended_texinfo(parser_t &parser)
{
    if (!parse_quark_comment(parser)) {
        // Parse extra Quake 2 surface info
        if (parser.parse_token(PARSE_OPTIONAL)) {
            texinfo_quake2_t q2_info;

            q2_info.contents = std::stoi(parser.token);

            if (parser.parse_token(PARSE_OPTIONAL)) {
                q2_info.flags = std::stoi(parser.token);
            }
            if (parser.parse_token(PARSE_OPTIONAL)) {
                q2_info.value = std::stoi(parser.token);
            }

            extended_info = q2_info;

            parse_quark_comment(parser);
        }
    }
}

void brush_side_t::set_texinfo(const texdef_quake_ed_t &texdef)
{
    texture_axis_t axis(plane);
    qvec3d vectors[2] = {axis.xv, axis.yv};

    /* Rotate axis */
    double ang = texdef.rotate / 180.0 * Q_PI;
    double sinv = sin(ang);
    double cosv = cos(ang);

    size_t sv, tv;

    if (vectors[0][0]) {
        sv = 0;
    } else if (vectors[0][1]) {
        sv = 1;
    } else {
        sv = 2; // unreachable, due to TextureAxisFromPlane lookup table
    }

    if (vectors[1][0]) {
        tv = 0; // unreachable, due to TextureAxisFromPlane lookup table
    } else if (vectors[1][1]) {
        tv = 1;
    } else {
        tv = 2;
    }

    for (size_t i = 0; i < 2; i++) {
        double ns = cosv * vectors[i][sv] - sinv * vectors[i][tv];
        double nt = sinv * vectors[i][sv] + cosv * vectors[i][tv];
        vectors[i][sv] = ns;
        vectors[i][tv] = nt;
    }

    for (size_t i = 0; i < 2; i++) {
        for (size_t j = 0; j < 3; j++) {
            /* Interpret zero scale as no scaling */
            vecs.at(i, j) = vectors[i][j] / (texdef.scale[i] ? texdef.scale[i] : 1);
        }
    }

    vecs.at(0, 3) = texdef.shift[0];
    vecs.at(1, 3) = texdef.shift[1];

    // TODO: move these self-tests somewhere else, do them for all types
#if 0
    if (false) {
        // Self-test of SetTexinfo_QuakeEd_New
        texvecf check;
        SetTexinfo_QuakeEd_New(plane, shift, rotate, scale, check);
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 4; j++) {
                if (fabs(check.at(i, j) - out->vecs.at(i, j)) > 0.001) {
                    SetTexinfo_QuakeEd_New(plane, shift, rotate, scale, check);
                    FError("fail");
                }
            }
        }
    }

    if (false) {
        // Self-test of TexDef_BSPToQuakeEd
        texdef_quake_ed_t reversed = TexDef_BSPToQuakeEd(plane, std::nullopt, out->vecs, planepts);

        if (!EqualDegrees(reversed.rotate, rotate)) {
            reversed.rotate += 180;
            reversed.scale[0] *= -1;
            reversed.scale[1] *= -1;
        }

        if (!EqualDegrees(reversed.rotate, rotate)) {
            ewt::print("wrong rotate got {} expected {}\n", reversed.rotate, rotate);
        }

        if (fabs(reversed.scale[0] - scale[0]) > 0.001 || fabs(reversed.scale[1] - scale[1]) > 0.001) {
            ewt::print("wrong scale, got {} {} exp {} {}\n", reversed.scale[0], reversed.scale[1], scale[0], scale[1]);
        }

        if (fabs(reversed.shift[0] - shift[0]) > 0.1 || fabs(reversed.shift[1] - shift[1]) > 0.1) {
            ewt::print("wrong shift, got {} {} exp {} {}\n", reversed.shift[0], reversed.shift[1], shift[0], shift[1]);
        }
    }
#endif
}

void brush_side_t::set_texinfo(const texdef_valve_t &texdef)
{
    for (size_t i = 0; i < 3; i++) {
        vecs.at(0, i) = texdef.axis.at(0, i) / texdef.scale[0];
        vecs.at(1, i) = texdef.axis.at(1, i) / texdef.scale[1];
    }

    vecs.at(0, 3) = texdef.shift[0];
    vecs.at(1, 3) = texdef.shift[1];
}

void brush_side_t::set_texinfo(const texdef_etp_t &texdef)
{
    qvec3d vectors[2];

    /*
     * Type 1 uses vecs[0] = (pt[2] - pt[0]) and vecs[1] = (pt[1] - pt[0])
     * Type 2 reverses the order of the vecs
     * 128 is the scaling factor assumed by QuArK.
     */
    if (!texdef.tx2) {
        vectors[0] = planepts[2] - planepts[0];
        vectors[1] = planepts[1] - planepts[0];
    } else {
        vectors[0] = planepts[1] - planepts[0];
        vectors[1] = planepts[2] - planepts[0];
    }

    vectors[0] *= 1.0 / 128.0;
    vectors[1] *= 1.0 / 128.0;

    double a = qv::dot(vectors[0], vectors[0]);
    double b = qv::dot(vectors[0], vectors[1]);
    double c = b; /* qv::dot(vectors[1], vectors[0]); */
    double d = qv::dot(vectors[1], vectors[1]);

    /*
     * Want to solve for out->vecs:
     *
     *    | a b | | out->vecs[0] | = | vecs[0] |
     *    | c d | | out->vecs[1] |   | vecs[1] |
     *
     * => | out->vecs[0] | = __ 1.0__  | d  -b | | vecs[0] |
     *    | out->vecs[1] |   a*d - b*c | -c  a | | vecs[1] |
     */
    double determinant = a * d - b * c;
    if (fabs(determinant) < ZERO_EPSILON) {
        logging::print("WARNING: {}: Face with degenerate QuArK-style texture axes\n", location);
        for (size_t i = 0; i < 3; i++) {
            vecs.at(0, i) = vecs.at(1, i) = 0;
        }
    } else {
        for (size_t i = 0; i < 3; i++) {
            vecs.at(0, i) = (d * vectors[0][i] - b * vectors[1][i]) / determinant;
            vecs.at(1, i) = -(a * vectors[1][i] - c * vectors[0][i]) / determinant;
        }
    }

    /* Finally, the texture offset is indicated by planepts[0] */
    for (size_t i = 0; i < 3; ++i) {
        vectors[0][i] = vecs.at(0, i);
        vectors[1][i] = vecs.at(1, i);
    }

    vecs.at(0, 3) = -qv::dot(vectors[0], planepts[0]);
    vecs.at(1, 3) = -qv::dot(vectors[1], planepts[0]);
}

/*
ComputeAxisBase()
from q3map2

computes the base texture axis for brush primitive texturing
note: ComputeAxisBase here and in editor code must always BE THE SAME!
warning: special case behaviour of atan2( y, x ) <-> atan( y / x ) might not be the same everywhere when x == 0
rotation by (0,RotY,RotZ) assigns X to normal
*/
inline std::tuple<qvec3d, qvec3d> compute_axis_base(const qvec3d &normal_unsanitized)
{
    double RotY, RotZ;
    qvec3d normal = normal_unsanitized;

    /* do some cleaning */
    if (fabs(normal[0]) < 1e-6) {
        normal[0] = 0.0f;
    }
    if (fabs(normal[1]) < 1e-6) {
        normal[1] = 0.0f;
    }
    if (fabs(normal[2]) < 1e-6) {
        normal[2] = 0.0f;
    }

    /* compute the two rotations around y and z to rotate x to normal */
    RotY = -atan2(normal[2], sqrt(normal[1] * normal[1] + normal[0] * normal[0]));
    RotZ = atan2(normal[1], normal[0]);

    return {/* rotate (0,1,0) and (0,0,1) to compute texX and texY */
        {-sin(RotZ), cos(RotZ), 0}, {/* the texY vector is along -z (t texture coorinates axis) */
                                        -sin(RotY) * cos(RotZ), -sin(RotY) * sin(RotZ), -cos(RotY)}};
}

void brush_side_t::set_texinfo(const texdef_bp_t &texdef)
{
    // FIXME:
    const int32_t texWidth = 64;
    const int32_t texHeight = 64;

    const auto [texX, texY] = compute_axis_base(plane.normal);
    const auto texMat = texdef.axis;

    /*
     derivation of the conversion below:

     classic BSP texture vecs to texture coordinates:

       u = (dot(vert, out->vecs[0]) + out->vecs[3]) / texWidth

     brush primitives: (starting with q3map2 code, then rearranging it to look like the classic formula)

       u = (texMat[0][0] * dot(vert, texX)) + (texMat[0][1] * dot(vert, texY)) + texMat[0][2]

     factor out vert:

       u = (vert[0] * (texX[0] * texMat[0][0] + texY[0] * texMat[0][1]))
          + (vert[1] * (texX[1] * texMat[0][0] + texY[1] * texMat[0][1]))
          + (vert[2] * (texX[2] * texMat[0][0] + texY[2] * texMat[0][1]))
          + texMat[0][2];

     multiplying that by 1 = (texWidth / texWidth) gives us something in the same shape as the classic formula,
     so we can get out->vecs.

     */

    vecs.at(0, 0) = texWidth * ((texX[0] * texMat.at(0, 0)) + (texY[0] * texMat.at(0, 1)));
    vecs.at(0, 1) = texWidth * ((texX[1] * texMat.at(0, 0)) + (texY[1] * texMat.at(0, 1)));
    vecs.at(0, 2) = texWidth * ((texX[2] * texMat.at(0, 0)) + (texY[2] * texMat.at(0, 1)));
    vecs.at(0, 3) = texWidth * texMat.at(0, 2);

    vecs.at(1, 0) = texHeight * ((texX[0] * texMat.at(1, 0)) + (texY[0] * texMat.at(1, 1)));
    vecs.at(1, 1) = texHeight * ((texX[1] * texMat.at(1, 0)) + (texY[1] * texMat.at(1, 1)));
    vecs.at(1, 2) = texHeight * ((texX[2] * texMat.at(1, 0)) + (texY[2] * texMat.at(1, 1)));
    vecs.at(1, 3) = texHeight * texMat.at(1, 2);
}

void brush_side_t::parse_texture_def(parser_t &parser, texcoord_style_t base_format)
{
    if (base_format == texcoord_style_t::brush_primitives) {
        raw = parse_bp(parser);

        parser.parse_token(PARSE_SAMELINE);
        texture = std::move(parser.token);
    } else if (base_format == texcoord_style_t::quaked) {
        parser.parse_token(PARSE_SAMELINE);
        texture = std::move(parser.token);

        parser.parse_token(PARSE_SAMELINE | PARSE_PEEK);

        if (parser.token == "[") {
            raw = parse_valve_220(parser);
        } else {
            raw = parse_quake_ed(parser);
        }
    } else {
        FError("{}: Bad brush format", parser.location);
    }

    // Read extra Q2 params and/or QuArK subtype
    parse_extended_texinfo(parser);

    std::visit([this](auto &&x) { set_texinfo(x); }, raw);
}

void brush_side_t::parse_plane_def(parser_t &parser)
{
    for (size_t i = 0; i < 3; i++) {
        if (i != 0) {
            parser.parse_token();
        }

        if (parser.token != "(") {
            goto parse_error;
        }

        for (size_t j = 0; j < 3; j++) {
            parser.parse_token(PARSE_SAMELINE);
            planepts[i][j] = std::stod(parser.token);
        }

        parser.parse_token(PARSE_SAMELINE);

        if (parser.token != ")") {
            goto parse_error;
        }
    }

    return;

parse_error:
    FError("{}: Invalid brush plane format", parser.location);
}

void brush_side_t::write_extended_info(std::ostream &stream)
{
    if (extended_info) {
        ewt::print(stream, " {} {} {}", extended_info->contents, extended_info->flags, extended_info->value);
    }
}

void brush_side_t::write_texinfo(std::ostream &stream, const texdef_quake_ed_t &texdef)
{
    ewt::print(
        stream, "{} {} {} {} {}", texdef.shift[0], texdef.shift[1], texdef.rotate, texdef.scale[0], texdef.scale[1]);
    write_extended_info(stream);
}

void brush_side_t::write_texinfo(std::ostream &stream, const texdef_valve_t &texdef)
{
    ewt::print(stream, "[ {} {} {} {} ] [ {} {} {} {} ] {} {} {}", texdef.axis.at(0, 0), texdef.axis.at(0, 1),
        texdef.axis.at(0, 2), texdef.shift[0], texdef.axis.at(1, 0), texdef.axis.at(1, 1), texdef.axis.at(1, 2),
        texdef.shift[1], texdef.rotate, texdef.scale[0], texdef.scale[1]);
    write_extended_info(stream);
}

void brush_side_t::write_texinfo(std::ostream &stream, const texdef_etp_t &texdef)
{
    write_texinfo(stream, (const texdef_quake_ed_t &)texdef);
    ewt::print(stream, "//TX{}", texdef.tx2 ? '2' : '1');
}

void brush_side_t::write_texinfo(std::ostream &stream, const texdef_bp_t &texdef)
{
    FError("todo bp");
}

void brush_side_t::write(std::ostream &stream)
{
    ewt::print(stream, "( {} {} {} ) ( {} {} {} ) ( {} {} {} ) {} ", planepts[0][0], planepts[0][1], planepts[0][2],
        planepts[1][0], planepts[1][1], planepts[1][2], planepts[2][0], planepts[2][1], planepts[2][2], texture);

    std::visit([this, &stream](auto &&x) { write_texinfo(stream, x); }, raw);
}

namespace convert_to_quaked
{
static qmat2x2f rotation2x2_deg(float degrees)
{
    float r = degrees * (Q_PI / 180.0);
    float cosr = cos(r);
    float sinr = sin(r);

    // [ cosTh -sinTh ]
    // [ sinTh cosTh  ]

    qmat2x2f M{cosr, sinr, // col 0
        -sinr, cosr}; // col1

    return M;
}

static float extractRotation(qmat2x2f m)
{
    qvec2f point = m * qvec2f(1, 0); // choice of this matters if there's shearing
    float rotation = atan2(point[1], point[0]) * 180.0 / Q_PI;
    return rotation;
}

static std::pair<int, int> getSTAxes(const qvec3d &snapped_normal)
{
    if (snapped_normal[0]) {
        return std::make_pair(1, 2);
    } else if (snapped_normal[1]) {
        return std::make_pair(0, 2);
    } else {
        return std::make_pair(0, 1);
    }
}

static qvec2f projectToAxisPlane(const qvec3d &snapped_normal, const qvec3d &point)
{
    const std::pair<int, int> axes = getSTAxes(snapped_normal);
    const qvec2f proj(point[axes.first], point[axes.second]);
    return proj;
}

float clockwiseDegreesBetween(qvec2f start, qvec2f end)
{
    start = qv::normalize(start);
    end = qv::normalize(end);

    const float cosAngle = std::max(-1.0f, std::min(1.0f, qv::dot(start, end)));
    const float unsigned_degrees = acos(cosAngle) * (360.0 / (2.0 * Q_PI));

    if (unsigned_degrees < ANGLEEPSILON)
        return 0;

    // get a normal for the rotation plane using the right-hand rule
    // if this is pointing up (qvec3f(0,0,1)), it's counterclockwise rotation.
    // if this is pointing down (qvec3f(0,0,-1)), it's clockwise rotation.
    qvec3f rotationNormal = qv::normalize(qv::cross(qvec3f(start[0], start[1], 0.0f), qvec3f(end[0], end[1], 0.0f)));

    const float normalsCosAngle = qv::dot(rotationNormal, qvec3f(0, 0, 1));
    if (normalsCosAngle >= 0) {
        // counterclockwise rotation
        return -unsigned_degrees;
    }
    // clockwise rotation
    return unsigned_degrees;
}

static texdef_quake_ed_t Reverse_QuakeEd(qmat2x2f M, const qplane3d &plane, bool preserveX)
{
    // Check for shear, because we might tweak M to remove it
    {
        qvec2f Xvec = M.row(0);
        qvec2f Yvec = M.row(1);
        double cosAngle = qv::dot(qv::normalize(Xvec), qv::normalize(Yvec));

        // const double oldXscale = sqrt(pow(M[0][0], 2.0) + pow(M[1][0], 2.0));
        // const double oldYscale = sqrt(pow(M[0][1], 2.0) + pow(M[1][1], 2.0));

        if (fabs(cosAngle) > 0.001) {
            // Detected shear

            if (preserveX) {
                const float degreesToY = clockwiseDegreesBetween(Xvec, Yvec);
                const bool CW = (degreesToY > 0);

                // turn 90 degrees from Xvec
                const qvec2f newYdir =
                    qv::normalize(qvec2f(qv::cross(qvec3f(0, 0, CW ? -1.0f : 1.0f), qvec3f(Xvec[0], Xvec[1], 0.0))));

                // scalar projection of the old Yvec onto newYDir to get the new Yscale
                const float newYscale = qv::dot(Yvec, newYdir);
                Yvec = newYdir * static_cast<float>(newYscale);
            } else {
                // Preserve Y.

                const float degreesToX = clockwiseDegreesBetween(Yvec, Xvec);
                const bool CW = (degreesToX > 0);

                // turn 90 degrees from Yvec
                const qvec2f newXdir =
                    qv::normalize(qvec2f(qv::cross(qvec3f(0, 0, CW ? -1.0f : 1.0f), qvec3f(Yvec[0], Yvec[1], 0.0))));

                // scalar projection of the old Xvec onto newXDir to get the new Xscale
                const float newXscale = qv::dot(Xvec, newXdir);
                Xvec = newXdir * static_cast<float>(newXscale);
            }

            // recheck
            cosAngle = qv::dot(qv::normalize(Xvec), qv::normalize(Yvec));
            if (fabs(cosAngle) > 0.001) {
                FError("SHEAR correction failed\n");
            }

            // update M
            M.at(0, 0) = Xvec[0];
            M.at(0, 1) = Xvec[1];

            M.at(1, 0) = Yvec[0];
            M.at(1, 1) = Yvec[1];
        }
    }

    // extract abs(scale)
    const double absXscale = sqrt(pow(M.at(0, 0), 2.0) + pow(M.at(0, 1), 2.0));
    const double absYscale = sqrt(pow(M.at(1, 0), 2.0) + pow(M.at(1, 1), 2.0));
    const qmat2x2f applyAbsScaleM{static_cast<float>(absXscale), // col0
        0,
        0, // col1
        static_cast<float>(absYscale)};

    auto [xv, yv, snapped_normal] = texture_axis_t(plane);

    const qvec2f sAxis = projectToAxisPlane(snapped_normal, xv);
    const qvec2f tAxis = projectToAxisPlane(snapped_normal, yv);

    // This is an identity matrix possibly with negative signs.
    const qmat2x2f axisFlipsM{sAxis[0], tAxis[0], // col0
        sAxis[1], tAxis[1]}; // col1

    // N.B. this is how M is built in SetTexinfo_QuakeEd_New and guides how we
    // strip off components of it later in this function.
    //
    //    qmat2x2f M = scaleM * rotateM * axisFlipsM;

    // strip off the magnitude component of the scale, and `axisFlipsM`.
    const qmat2x2f flipRotate = qv::inverse(applyAbsScaleM) * M * qv::inverse(axisFlipsM);

    // We don't know the signs on the scales, which will mess up figuring out the rotation, so try all 4 combinations
    for (float xScaleSgn : std::vector<float>{-1.0, 1.0}) {
        for (float yScaleSgn : std::vector<float>{-1.0, 1.0}) {

            // "apply" - matrix constructed to apply a guessed value
            // "guess" - this matrix might not be what we think

            const qmat2x2f applyGuessedFlipM{xScaleSgn, // col0
                0,
                0, // col1
                yScaleSgn};

            const qmat2x2f rotateMGuess = qv::inverse(applyGuessedFlipM) * flipRotate;
            const float angleGuess = extractRotation(rotateMGuess);

            //            const qmat2x2f Mident = rotateMGuess * rotation2x2_deg(-angleGuess);

            const qmat2x2f applyAngleGuessM = rotation2x2_deg(angleGuess);
            const qmat2x2f Mguess = applyGuessedFlipM * applyAbsScaleM * applyAngleGuessM * axisFlipsM;

            if (fabs(M.at(0, 0) - Mguess.at(0, 0)) < 0.001 && fabs(M.at(1, 0) - Mguess.at(1, 0)) < 0.001 &&
                fabs(M.at(0, 1) - Mguess.at(0, 1)) < 0.001 && fabs(M.at(1, 1) - Mguess.at(1, 1)) < 0.001) {

                texdef_quake_ed_t reversed;
                reversed.rotate = angleGuess;
                reversed.scale[0] = xScaleSgn / absXscale;
                reversed.scale[1] = yScaleSgn / absYscale;
                return reversed;
            }
        }
    }

    // TODO: detect when we expect this to fail, i.e.  invalid texture axes (0-length),
    // and throw an error if it fails unexpectedly.

    return {};
}

static qmat4x4f texVecsTo4x4Matrix(const qplane3d &faceplane, const texvecf &in_vecs)
{
    //           [s]
    // T * vec = [t]
    //           [distOffPlane]
    //           [?]

    qmat4x4f T{
        in_vecs.at(0, 0), in_vecs.at(1, 0), static_cast<float>(faceplane.normal[0]), 0, // col 0
        in_vecs.at(0, 1), in_vecs.at(1, 1), static_cast<float>(faceplane.normal[1]), 0, // col 1
        in_vecs.at(0, 2), in_vecs.at(1, 2), static_cast<float>(faceplane.normal[2]), 0, // col 2
        in_vecs.at(0, 3), in_vecs.at(1, 3), static_cast<float>(-faceplane.dist), 1 // col 3
    };
    return T;
}

static qvec2f evalTexDefAtPoint(const texdef_quake_ed_t &texdef, const qplane3d &faceplane, const qvec3f &point)
{
    brush_side_t temp;
    temp.set_texinfo(texdef_quake_ed_t{texdef.shift, texdef.rotate, texdef.scale});

    const qmat4x4f worldToTexSpace_res = texVecsTo4x4Matrix(faceplane, temp.vecs);
    const qvec2f uv = qvec2f(worldToTexSpace_res * qvec4f(point[0], point[1], point[2], 1.0f));
    return uv;
}

static texdef_quake_ed_t addShift(const texdef_quake_ed_t &texdef, const qvec2f shift)
{
    texdef_quake_ed_t res = texdef;
    res.shift = shift;
    return res;
}

qvec2f normalizeShift(const std::optional<img::texture_meta> &texture, const qvec2f &in)
{
    if (!texture) {
        return in; // can't do anything without knowing the texture size.
    }

    int fullWidthOffsets = static_cast<int>(in[0]) / texture->width;
    int fullHeightOffsets = static_cast<int>(in[1]) / texture->height;

    qvec2f result(in[0] - static_cast<float>(fullWidthOffsets * texture->width),
        in[1] - static_cast<float>(fullHeightOffsets * texture->height));
    return result;
}

/// `texture` is optional. If given, the "shift" values can be normalized
static texdef_quake_ed_t TexDef_BSPToQuakeEd(const qplane3d &faceplane, const std::optional<img::texture_meta> &texture,
    const texvecf &in_vecs, const std::array<qvec3d, 3> &facepoints)
{
    // First get the un-rotated, un-scaled unit texture vecs (based on the face plane).
    texture_axis_t axis(faceplane);
    qvec3d &snapped_normal = axis.snapped_normal;

    const qmat4x4f worldToTexSpace = texVecsTo4x4Matrix(faceplane, in_vecs);

    // Grab the UVs of the 3 reference points
    qvec2f facepoints_uvs[3];
    for (int i = 0; i < 3; i++) {
        facepoints_uvs[i] = qvec2f(worldToTexSpace * qvec4f(facepoints[i][0], facepoints[i][1], facepoints[i][2], 1.0));
    }

    // Project the 3 reference points onto the axis plane. They are now 2d points.
    qvec2f facepoints_projected[3];
    for (int i = 0; i < 3; i++) {
        facepoints_projected[i] = projectToAxisPlane(snapped_normal, facepoints[i]);
    }

    // Now make 2 vectors out of our 3 points (so we are ignoring translation for now)
    const qvec2f p0p1 = facepoints_projected[1] - facepoints_projected[0];
    const qvec2f p0p2 = facepoints_projected[2] - facepoints_projected[0];

    const qvec2f p0p1_uv = facepoints_uvs[1] - facepoints_uvs[0];
    const qvec2f p0p2_uv = facepoints_uvs[2] - facepoints_uvs[0];

    /*
    Find a 2x2 transformation matrix that maps p0p1 to p0p1_uv, and p0p2 to p0p2_uv

    [ a b ] [ p0p1.x ] = [ p0p1_uv.x ]
    [ c d ] [ p0p1.y ]   [ p0p1_uv.y ]

    [ a b ] [ p0p2.x ] = [ p0p1_uv.x ]
    [ c d ] [ p0p2.y ]   [ p0p2_uv.y ]

    writing as a system of equations:

    a * p0p1.x + b * p0p1.y = p0p1_uv.x
    c * p0p1.x + d * p0p1.y = p0p1_uv.y
    a * p0p2.x + b * p0p2.y = p0p2_uv.x
    c * p0p2.x + d * p0p2.y = p0p2_uv.y

    back to a matrix equation, with the unknowns in a column vector:

    [ p0p1_uv.x ]   [ p0p1.x p0p1.y 0       0      ] [ a ]
    [ p0p1_uv.y ] = [ 0       0     p0p1.x p0p1.y  ] [ b ]
    [ p0p2_uv.x ]   [ p0p2.x p0p2.y 0       0      ] [ c ]
    [ p0p2_uv.y ]   [ 0       0     p0p2.x p0p2.y  ] [ d ]

    */

    const qmat4x4f M{
        p0p1[0], 0, p0p2[0], 0, // col 0
        p0p1[1], 0, p0p2[1], 0, // col 1
        0, p0p1[0], 0, p0p2[0], // col 2
        0, p0p1[1], 0, p0p2[1] // col 3
    };

    const qmat4x4f Minv = qv::inverse(M);
    const qvec4f abcd = Minv * qvec4f(p0p1_uv[0], p0p1_uv[1], p0p2_uv[0], p0p2_uv[1]);

    const qmat2x2f texPlaneToUV{abcd[0], abcd[2], // col 0
        abcd[1], abcd[3]}; // col 1

    {
        // self check
        //        qvec2f uv01_test = texPlaneToUV * p0p1;
        //        qvec2f uv02_test = texPlaneToUV * p0p2;

        // these fail if one of the texture axes is 0 length.
        //        checkEq(uv01_test, p0p1_uv, 0.01);
        //        checkEq(uv02_test, p0p2_uv, 0.01);
    }

    const texdef_quake_ed_t res = Reverse_QuakeEd(texPlaneToUV, faceplane, false);

    // figure out shift based on facepoints[0]
    const qvec3f testpoint = facepoints[0];
    qvec2f uv0_actual = evalTexDefAtPoint(addShift(res, qvec2f(0, 0)), faceplane, testpoint);
    qvec2f uv0_desired = qvec2f(worldToTexSpace * qvec4f(testpoint[0], testpoint[1], testpoint[2], 1.0f));
    qvec2f shift = uv0_desired - uv0_actual;

    // sometime we have very large shift values, normalize them to be smaller
    shift = normalizeShift(texture, shift);

    const texdef_quake_ed_t res2 = addShift(res, shift);
    return res2;
}
}; // namespace convert_to_quaked

namespace convert_to_valve
{
static texdef_valve_t TexDef_BSPToValve(const texvecf &in_vecs)
{
    texdef_valve_t res;

    // From the valve -> bsp code,
    //
    //    for (i = 0; i < 3; i++) {
    //        out->vecs[0][i] = axis[0][i] / scale[0];
    //        out->vecs[1][i] = axis[1][i] / scale[1];
    //    }
    //
    // We'll generate axis vectors of length 1 and pick the necessary scale

    for (size_t i = 0; i < 2; i++) {
        qvec3d axis = in_vecs.row(i).xyz();
        const double length = qv::normalizeInPlace(axis);
        // avoid division by 0
        if (length != 0.0) {
            res.scale[i] = 1.0 / length;
        } else {
            res.scale[i] = 0.0;
        }
        res.shift[i] = in_vecs.at(i, 3);
        res.axis.set_row(i, axis);
    }
    res.rotate = 0.0;

    return res;
}
}; // namespace convert_to_valve

namespace convert_to_bp
{
// From FaceToBrushPrimitFace in GtkRadiant
static texdef_bp_t TexDef_BSPToBrushPrimitives(
    const qplane3d &plane, const img::texture_meta &texture, const texvecf &in_vecs)
{
    auto [texX, texY] = compute_axis_base(plane.normal);

    // compute projection vector
    qvec3d proj = plane.normal * plane.dist;

    // (0,0) in plane axis base is (0,0,0) in world coordinates + projection on the affine plane
    // (1,0) in plane axis base is texX in world coordinates + projection on the affine plane
    // (0,1) in plane axis base is texY in world coordinates + projection on the affine plane
    // use old texture code to compute the ST coords of these points
    qvec2d st[] = {in_vecs.uvs(proj, texture.width, texture.height),
        in_vecs.uvs(texX + proj, texture.width, texture.height),
        in_vecs.uvs(texY + proj, texture.width, texture.height)};
    // compute texture matrix
    texdef_bp_t res;
    res.axis.set_col(2, st[0]);
    res.axis.set_col(0, st[1] - st[0]);
    res.axis.set_col(1, st[2] - st[0]);
    return res;
}
}; // namespace convert_to_bp

void brush_side_t::convert_to(texcoord_style_t style, const gamedef_t *game, const settings::common_settings &options)
{
    // we're already this style
    switch (style) {
        case texcoord_style_t::quaked:
            if (std::holds_alternative<texdef_quake_ed_t>(raw)) {
                return;
            }
            break;
        case texcoord_style_t::etp:
            if (std::holds_alternative<texdef_etp_t>(raw)) {
                return;
            }
            break;
        case texcoord_style_t::brush_primitives:
            if (std::holds_alternative<texdef_bp_t>(raw)) {
                return;
            }
            break;
        case texcoord_style_t::valve_220:
            if (std::holds_alternative<texdef_valve_t>(raw)) {
                return;
            }
            break;
    }

    if (style == texcoord_style_t::quaked) {
        std::optional<img::texture_meta> meta = std::nullopt;

        if (game) {
            meta = std::get<0>(img::load_texture_meta(texture, game, options));
        }

        raw = convert_to_quaked::TexDef_BSPToQuakeEd(plane, meta, vecs, planepts);
    } else if (style == texcoord_style_t::valve_220) {
        raw = convert_to_valve::TexDef_BSPToValve(vecs);
    } else if (style == texcoord_style_t::brush_primitives) {
        if (!game) {
            FError("conversion to brush primitives requires a `--game` option to be set");
        }

        auto [meta, result, data] = img::load_texture_meta(texture, game, options);

        if (!meta) {
            FError("conversion to brush primitives requires texture to be loaded");
        }

        raw = convert_to_bp::TexDef_BSPToBrushPrimitives(plane, meta.value(), vecs);
    } else {
        FError("can't currently convert to this format!");
    }
}

void brush_t::parse_brush_face(parser_t &parser, texcoord_style_t base_format)
{
    brush_side_t side;

    side.location = parser.location;

    side.parse_plane_def(parser);

    /* calculate the normal/dist plane equation */
    qvec3d ab = side.planepts[0] - side.planepts[1];
    qvec3d cb = side.planepts[2] - side.planepts[1];

    double length;
    qvec3d normal = qv::normalize(qv::cross(ab, cb), length);
    double dist = qv::dot(side.planepts[1], normal);

    side.plane = {normal, dist};

    side.parse_texture_def(parser, base_format);

    if (length < NORMAL_EPSILON) {
        logging::print("WARNING: {}: Brush plane with no normal\n", parser.location);
        return;
    }

    /* Check for duplicate planes */
    for (auto &check : faces) {
        if (qv::epsilonEqual(check.plane, side.plane) || qv::epsilonEqual(-check.plane, side.plane)) {
            logging::print("{}: Brush with duplicate plane\n", parser.location);
            return;
        }
    }

    // ericw -- round texture vector values that are within ZERO_EPSILON of integers,
    // to attempt to attempt to work around corrupted lightmap sizes in DarkPlaces
    // (it uses 32 bit precision in CalcSurfaceExtents)
    for (size_t i = 0; i < 2; i++) {
        for (size_t j = 0; j < 4; j++) {
            double r = Q_rint(side.vecs.at(i, j));
            if (fabs(side.vecs.at(i, j) - r) < ZERO_EPSILON) {
                side.vecs.at(i, j) = r;
            }
        }
    }

    side.validate_texture_projection();

    faces.push_back(std::move(side));
}

void brush_t::write(std::ostream &stream)
{
    stream << "{\n";

    if (base_format == texcoord_style_t::brush_primitives) {
        stream << "brushDef\n{\n";
    }

    for (auto &face : faces) {
        face.write(stream);
        stream << "\n";
    }

    if (base_format == texcoord_style_t::brush_primitives) {
        stream << "}\n";
    }

    stream << "}\n";
}

void brush_t::convert_to(texcoord_style_t style, const gamedef_t *game, const settings::common_settings &options)
{
    for (auto &face : faces) {
        face.convert_to(style, game, options);
    }

    if (style == texcoord_style_t::brush_primitives) {
        base_format = style;
    } else {
        base_format = texcoord_style_t::quaked;
    }
}

// map file stuff

void map_entity_t::parse_entity_dict(parser_t &parser)
{
    std::string key = std::move(parser.token);

    // trim whitespace from start/end
    while (std::isspace(key.front())) {
        key.erase(key.begin());
    }
    while (std::isspace(key.back())) {
        key.erase(key.end() - 1);
    }

    parser.parse_token(PARSE_SAMELINE);
    epairs.set(key, parser.token);
}

void map_entity_t::parse_brush(parser_t &parser)
{
    // ericw -- brush primitives
    if (!parser.parse_token(PARSE_PEEK)) {
        FError("{}: unexpected EOF after {{ beginning brush", parser.location);
    }

    brush_t brush;

    if (parser.token == "(" || parser.token == "}") {
        brush.base_format = texcoord_style_t::quaked;
    } else {
        parser.parse_token();
        brush.base_format = texcoord_style_t::brush_primitives;

        // optional
        if (parser.token == "brushDef") {
            if (!parser.parse_token()) {
                FError("{}: Brush primitives: unexpected EOF (nothing after brushDef)", parser.location);
            }
        }

        // mandatory
        if (parser.token != "{") {
            FError("{}: Brush primitives: expected second {{ at beginning of brush, got \"{}\"", parser.location,
                parser.token);
        }
    }
    // ericw -- end brush primitives

    while (parser.parse_token()) {

        // set linenum after first parsed token
        if (!brush.location) {
            brush.location = parser.location;
        }

        if (parser.token == "}") {
            break;
        }

        brush.parse_brush_face(parser, brush.base_format);
    }

    // ericw -- brush primitives - there should be another closing }
    if (brush.base_format == texcoord_style_t::brush_primitives) {
        if (!parser.parse_token()) {
            FError("Brush primitives: unexpected EOF (no closing brace)");
        } else if (parser.token != "}") {
            FError("{}: Brush primitives: Expected }}, got: {}", parser.location, parser.token);
        }
    }
    // ericw -- end brush primitives

    if (brush.faces.size()) {
        brushes.push_back(std::move(brush));
    }
}

bool map_entity_t::parse(parser_t &parser)
{
    location = parser.location;

    if (!parser.parse_token()) {
        return false;
    }

    if (parser.token != "{") {
        FError("{}: Invalid entity format, {{ not found", parser.location);
    }

    do {
        if (!parser.parse_token()) {
            FError("Unexpected EOF (no closing brace)");
        }

        if (parser.token == "}") {
            break;
        } else if (parser.token == "{") {
            parse_brush(parser);
        } else {
            parse_entity_dict(parser);
        }
    } while (1);

    return true;
}

void map_entity_t::write(std::ostream &stream)
{
    stream << "{\n";

    for (auto &kvp : epairs) {
        ewt::print(stream, "\"{}\" \"{}\"\n", kvp.first, kvp.second);
    }

    size_t brush_id = 0;

    for (auto &brush : brushes) {
        ewt::print(stream, "// brush {}\n", brush_id++);
        brush.write(stream);
    }

    stream << "}\n";
}

void map_file_t::parse(parser_t &parser)
{
    while (true) {
        map_entity_t &entity = entities.emplace_back();

        if (!entity.parse(parser)) {
            break;
        }
    }

    // Remove dummy entity inserted above
    assert(!entities.back().epairs.size());
    entities.pop_back();
}

void map_file_t::write(std::ostream &stream)
{
    size_t ent_id = 0;

    for (auto &entity : entities) {
        ewt::print(stream, "// entity {}\n", ent_id++);
        entity.write(stream);
    }
}

void map_file_t::convert_to(texcoord_style_t style, const gamedef_t *game, const settings::common_settings &options)
{
    for (auto &entity : entities) {
        for (auto &brush : entity.brushes) {
            brush.convert_to(style, game, options);
        }
    }
}

map_file_t parse(std::string_view view, parser_source_location base_location)
{
    parser_t parser(view, base_location);

    map_file_t result;
    result.parse(parser);
    return result;
}

map_file_t parse(const fs::data &data, parser_source_location base_location)
{
    parser_t parser(data, base_location);

    map_file_t result;
    result.parse(parser);
    return result;
}

} // namespace mapfile
