// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
//
#include <iostream>
#include <vector>
#include <algorithm>
#include <iostream>
#include <fstream>
#include <chrono>
#include <filesystem>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include "opencv2/objdetect.hpp"
#include <opencv2/dnn.hpp>

#include "mq_worker.h"
#if ENABLE_GRPC
#include "fileprocess.h"
#endif
#include "http.h"
#include "libqr.h"

static
int detect_cmd(char **argv, int argc)
{
    char *file = argv[0];
    auto orig = imread(file);
    QRCodeDetector detector;

    Mat points;
    Mat straight;
    auto r = detector.detectAndDecode(orig, points, straight);
    printf("r: %s\n", r.c_str());
    printf("points: %d %d\n", points.rows, points.cols);
    for (int i = 0; i < points.cols; i++) {
        printf("%f ", points.at<float>(0, i));
    }
    printf("\n");
    return 0;
}

static
int angle_cmd(char **argv, int argc)
{
    char *file = argv[0];
    printf("file: %s\n", file);
    Mat orig = imread(file);
    string qrcode, err;
    float angle;
    ProcessState ps;
    auto r = emblem_dot_angle(ps, orig, angle, qrcode, err);

    if (!r) {
        cerr << r << ":" << err << endl;
        return 1;
    }
    printf("angle: %.1f\n", angle);
    printf("qrcode: %s\n", qrcode.c_str());
    return 0;
}

static
int dot_cmd(char **argv, int argc)
{
    ProcessState ps;
    char *file = argv[0];
    printf("file: %s\n", file);
    Mat orig = imread(file);
    string qrcode, err;
    float angle;
    auto r = emblem_dot_angle(ps, orig, angle, qrcode, err);

    if (!r) {
        cerr << r << ":" << err << endl;
        return 1;
    }
    string outfile = string(file) + ".dot.jpg";
    printf("angle: %.1f\n", angle);
    printf("qrcode: %s\n", qrcode.c_str());
    printf("saving dot file: %s\n", outfile.c_str());
    imwrite(outfile, ps.dot_area);
    return 0;
}

static
int clarity_cmd(char **argv, int argc)
{
    string err;

    char *file = argv[0];
    printf("file: %s\n", file);
    Mat orig = imread(file);
    Mat gray;
    cvtColor(orig, gray, COLOR_BGR2GRAY);
    auto c = laplacian(gray, err);
    printf("clarity: %lf\n", c);

    return 0;
}

static
int rectify_cmd(char **argv, int argc)
{
    char *file = argv[0];
    string err;
    ProcessState ps;
    Mat orig = imread(file);

    ps.orig = &orig;
    preprocess(ps);

    if (!detect_qr(ps, 0.20, false, err)) {
        cerr << err << endl;
        return 1;
    }
    string outfile = string(file) + ".qr.jpg";
    imwrite(outfile, ps.straighten);
    return 0;
}

static
int topleft_cmd(char **argv, int argc)
{
    char *file = argv[0];
    string err;
    ProcessState ps;
    Mat orig = imread(file);

    ps.orig = &orig;
    preprocess(ps);

    if (!detect_qr(ps, 0.02, true, err)) {
        cerr << err << endl;
        return 1;
    }
    string outfile = string(file) + ".topleft.jpg";
    Mat &base = ps.straighten;
    auto crop = Rect(0, 0, base.cols / 2, base.rows / 2);
    Mat result = base(crop);
    imwrite(outfile, result);
    return 0;
}

static
int find_roi_start_point(Mat &bin, Point &p)
{
    int npoints = 4;

    for (int i = 0; i < bin.cols / 3; i++) {
        uchar sum = 0;
        for (int j = 0; j < npoints; j++) {
            int v = i + j;
            sum += bin.at<uchar>(v, v);
        }
        if (sum == 0) {
            p = Point(i, i);
            return 0;
        }
    }
    cerr << "find_roi_start_point" << endl;
    return -1;
}

static
vector<int> count_black(Mat &bin, bool count_rows, int size)
{
    vector<int> ret;
    for (int i = 0; i < size; i++) {
        int count = 0;
        for (int j = 0; j < size; j++) {
            int x = count_rows ? j : i;
            int y = count_rows ? i : j;
            if (bin.at<uchar>(y, x) == 0) count++;
        }
        ret.push_back(count);
    }
    return ret;
}

static int find_start_of_first_black_range(const vector<int> &data)
{
    size_t i = 0;
    int m = *std::max_element(data.begin(), data.end());
    int thres = m * 50 / 100;
    while (i < data.size() - 3) {
        if (data[i] >= thres && data[i + 1] >= thres && data[i + 2] >= thres) {
            break;
        }
        i++;
    }
    if (i >= data.size() - 3) return -1;
    return i;
}

static int find_end_of_first_black_range(const vector<int> &data)
{
    size_t i = 0;
    int m = *std::max_element(data.begin(), data.end());
    int thres = m * 50 / 100;
    while (i < data.size() - 3) {
        if (data[i] >= thres && data[i + 1] >= thres && data[i + 2] >= thres) {
            break;
        }
        i++;
    }
    if (i >= data.size() - 3) return -1;
    while (i < data.size() - 3) {
        if (data[i] < thres || data[i + 1] < thres || data[i + 2] < thres) {
            break;
        }
        i++;
    }
    if (i >= data.size() - 3) return -1;
    return i;
}

static
int find_roi_rect(Mat &bin, Point &start, Rect &rect, bool inner, string &err)
{
    Mat visited;
    bin.copyTo(visited);
    vector<Point> q;
    q.push_back(start);
    int min_x = bin.rows, min_y = bin.rows, max_x = 0, max_y = 0;
    int orig_size = max(bin.rows, bin.cols);

    while (q.size()) {
        Point p = q.back();
        q.pop_back();
        visited.at<uchar>(p.y, p.x) = 255;
        min_x = min(min_x, p.x);
        min_y = min(min_y, p.y);
        max_x = max(max_x, p.x);
        max_y = max(max_y, p.y);
        for (int xoff = -1; xoff <= 1; xoff++) {
            for (int yoff = -1; yoff <= 1; yoff++) {
                int x = p.x + xoff;
                int y = p.y + yoff;
                if (x >= 0 && x < visited.cols && y >= 0 && y < visited.rows) {
                    auto v = visited.at<uchar>(y, x);
                    if (v == 0) {
                        if (q.size() >= 20000) {
                            err = string("roi detected range too large: ") + to_string(q.size());
                            return -1;
                        }
                        q.push_back(Point(x, y));
                        visited.at<uchar>(y, x) = 255;
                    }
                }
            }
        }
    }
    if (max_x - min_x < 50 || max_y - min_y < 50) {
        err = "detected roi outer region too small";
        return -1;
    }

    auto size = std::max(max_x, max_y);

    auto row_sums = count_black(bin, true, size);
    auto col_sums = count_black(bin, false, size);

    min_x = inner ? find_end_of_first_black_range(col_sums) : find_start_of_first_black_range(col_sums);
    min_y = inner ? find_end_of_first_black_range(row_sums) : find_start_of_first_black_range(row_sums);

    if (min_x < 0 || min_y < 0) {
        err = "min_x or min_y is negative";
        return -1;
    }

    // find the max values, similarly
    std::reverse(col_sums.begin(), col_sums.end());
    std::reverse(row_sums.begin(), row_sums.end());
    max_x = size - (inner ? find_end_of_first_black_range(col_sums) : find_start_of_first_black_range(col_sums));
    max_y = size - (inner ? find_end_of_first_black_range(row_sums) : find_start_of_first_black_range(row_sums));

    if (max_x < 0 || max_y < 0) return -1;

    if (max_x - min_x < 50 || max_y - min_y < 50) {
        err = "detected roi region too small";
        return -1;
    }
    size = (max_x - min_x + max_y - min_y) / 2;

    if (size < orig_size / 5 || size > orig_size * 3 / 5) {
        err = "size of found region is out of valid range";
        return -1;
    }
    rect = Rect(min_x + 1, min_y + 1, size, size);

    return 0;
}

static void get_bin(Mat &orig, Mat &out)
{
    Mat gray;
    Mat filtered;
    Point start;
    Rect roi_rect;
    cvtColor(orig, gray, COLOR_BGR2GRAY);
    convertScaleAbs(gray, gray, 2);
    // bilateralFilter(gray, filtered, 9, 150, 150, BORDER_DEFAULT);
    medianBlur(gray, gray, 9);
    adaptiveThreshold(gray, out, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 11, 2);
}

static
int find_roi(Mat &qr_straighten, Mat &roi, bool inner, string &err)
{
    Mat bin;
    Rect topleft_r(0, 0, qr_straighten.cols / 2, qr_straighten.rows / 2);
    Mat topleft = qr_straighten(topleft_r);
    get_bin(topleft, bin);

    Point start;
    Rect roi_rect;
    auto r = find_roi_start_point(bin, start);
    if (r) {
        err = "failed to find roi start point";
        return r;
    }
    r = find_roi_rect(bin, start, roi_rect, inner, err);
    if (r) {
        return r;
    }
    roi = qr_straighten(roi_rect);
    return 0;
}

int roi_process_one(const char *file, bool inner, string &err, bool warp, Mat *roi_out)
{
    Mat roi;
    Mat orig = imread(file, IMREAD_COLOR);
    Mat qr_with_margin;

    if (warp) {
        ProcessState ps;
        ps.orig = &orig;
        preprocess(ps);
        if (!detect_qr(ps, 0.02, true, err)) {
            cerr << err << ":" << file << endl;
            return 1;
        }
        qr_with_margin = ps.straighten;
    } else {
        qr_with_margin = orig;
    }
    if (qr_with_margin.cols <= 0 || qr_with_margin.rows <= 0) return -1;
    auto r = find_roi(qr_with_margin, roi, inner, err);
    if (r) return r;
    if (roi_out) {
        *roi_out = roi;
    }
    return 0;
}

static bool is_dir(char *path)
{
    struct stat st;

    if (lstat(path, &st) == 0) {
        if (S_ISREG(st.st_mode)) {
            return false;
        } else if (S_ISDIR(st.st_mode)) {
            return true;
        } else {
            return false;
        }
    } else {
        perror("Error in lstat");
    }
    return false;
}

static void save_roi(string orig_file, Mat &roi)
{
    string outfile = orig_file + ".roi.jpg";
    cout << "save: " << outfile << endl;
    imwrite(outfile, roi);
}

static
int frame_roi_cmd(char **argv, int argc)
{
    char *file = argv[0];
    string err;
    int ret = 0;
    Mat roi;

    cout << "frame roi processing: " << file << endl;
    ret = roi_process_one(file, false, err, true, &roi);
    if (ret) {
        cerr << "failed to process: " << file << ":" << err <<endl;
        return ret;
    }
    save_roi(file, roi);
    return ret;
}

static
int roi_cmd(char **argv, int argc)
{
    char *file = argv[0];
    string err;
    int ret = 0;

    cout << "roi processing: " << file << endl;
    if (is_dir(file)) {
        for (auto const& dir_entry : filesystem::directory_iterator{file}) {
            auto path = dir_entry.path();
            Mat roi;
            if (roi_process_one(path.c_str(), false, err, false, &roi) != 0) {
                cerr << "failed: " << path << ":" << err << endl;
                ret = 1;
            }
            save_roi(file, roi);
        }
    } else {
        Mat roi;
        ret = roi_process_one(file, false, err, false, &roi);
        if (ret) {
            cerr << "failed to process: " << file << ":" << err <<endl;
        } else {
            save_roi(file, roi);
        }
    }

    return ret;
}

static
int roi_bench_cmd(char **argv, int argc)
{
    char *file = argv[0];
    int n = 0;
    string err;
    auto begin = chrono::system_clock::now();
    for (auto const& dir_entry : filesystem::directory_iterator{file}) {
        auto path = dir_entry.path();
        if (roi_process_one(path.c_str(), false, err, false, NULL) == 0) {
            n += 1;
        }
    }
    auto end = chrono::system_clock::now();
    std::chrono::duration<float> duration = end - begin;
    float seconds = duration.count();
    printf("qps: %.1f\n", n / seconds);
    return 0;
}

#if USE_PULSAR
static vector<string> split_path(const string &path)
{
    vector<string> ret;
    string cur = "";
    for (auto x: path) {
        if (x == '/') {
            if (cur.size()) {
                ret.push_back(cur);
                cur = "";
            }
        } else {
            cur += x;
        }
    }
    if (cur.size()) {
        ret.push_back(cur);
        cur = "";
    }
    return ret;
}

static string join_path(const vector<string> fs)
{
    string ret;
    const string sep = "/";

    for (auto x: fs) {
        ret += sep + x;
    }
    if (ret.size() == 0) ret = "/";
    return ret;
}

static string get_output_path(const string &path)
{
    auto ret = split_path(path);
    ret[3] = "roi";
    return join_path(ret);
}

static
int roi_worker_handle_image(const string &input_path,
                            const vector<uint8_t> &input,
                            string &output_path,
                            vector<uint8_t> &output)
{
    Mat roi;
    Mat orig = imdecode(input, IMREAD_COLOR);

    auto r = find_roi(orig, roi, true, true);
    if (r) return r;
    imencode(".jpg", roi, output);
    output_path = get_output_path(input_path);
    return 0;
}

static
int roi_worker_handle_image_nop(const string &input_path,
                                const vector<uint8_t> &input,
                                string &output_path,
                                vector<uint8_t> &output)
{
    output.push_back('f');
    output.push_back('o');
    output.push_back('o');
    output_path = get_output_path(input_path);
    return 0;
}

static
int roi_worker_cmd(char *topic)
{
    string worker_name = "roi-worker-";
    worker_name += to_string(rand());
    return mq_worker(topic, "roi-worker", roi_worker_handle_image);
}

static
int roi_worker_nop_cmd(char *topic)
{
    string worker_name = "roi-worker-";
    worker_name += to_string(rand());
    return mq_worker(topic, "roi-worker", roi_worker_handle_image_nop);
}

#endif

#if ENABLE_GRPC
static
int grpc_server_cmd(char *addr)
{
    return run_server(addr, roi_worker_handle_image);
}
#endif

static
int http_server_handle_image(const vector<uint8_t> &input,
                             vector<uint8_t> &output)
{
    string err;
    Mat roi;
    Mat orig = imdecode(input, IMREAD_COLOR);

    if (orig.empty()) {
        return -EINVAL;
    }
    auto r = find_roi(orig, roi, false, err);
    if (r) return r;
    imencode(".jpg", roi, output);
    return 0;
}

static
int http_server_cmd(char **argv, int argc)
{
    char *port = argv[0];
    return start_http_server(atoi(port), http_server_handle_image);
}

static
int verify_cmd(char **args, int nargs)
{
    char *std_file = args[0];
    char *frame_file = args[1];
    Mat std = imread(std_file);
    Mat frame = imread(frame_file);
    Mat roi;
    int r;

    string err;
    r = roi_process_one(frame_file, false, err, true, &roi);
    if (r) {
        printf("failed to find roi: %s\n", err.c_str());
        return r;
    }
    double s = emblem_roi_similarity(FuzzyPixelCmp, std, roi, err);

    if (err.size()) {
        printf("err: %s\n", err.c_str());
        return 1;
    } else {
        printf("similarity: %f\n", s); return 0;
        return 0;
    }
}

static
void usage(const char *name, vector<string> &cmds)
{
    printf("usage: %s <cmd> <arg0>\n", name);
    printf("or for 2 args: %s <cmd> <arg0> <arg1>\n", name);
    printf("possible commands:\n");
    for (auto cmd: cmds) {
        printf("  %s\n", cmd.c_str());
    }
}

static
int roi_verify_cmd(char **argv, int argc)
{
    char *model_path = argv[0];
    char *input_file = argv[1];
    Mat input_img = imread(input_file);
    cv::dnn::Net net = cv::dnn::readNetFromONNX(model_path);
    if (net.empty()) {
        std::cerr << "Failed to load ONNX model!" << std::endl;
        return -1;
    }
    cv::Mat blob;
    cv::resize(input_img, input_img, cv::Size(128, 64));  // 调整图像大小
    cv::dnn::blobFromImage(input_img, blob, 1.0 / 255.0, cv::Size(64, 128), cv::Scalar(0.485, 0.456, 0.406), true, false);
    blob = (blob - cv::Scalar(0.485, 0.456, 0.406)) / cv::Scalar(0.229, 0.224, 0.225);  // 归一化
    net.setInput(blob);
    cv::Mat output = net.forward();
    float* data = (float*)output.data;
    int class_id = std::max_element(data, data + output.total()) - data;  // 找到最大概率的类别
    float confidence = data[class_id];
    std::cout << "Predicted class: " << class_id << ", Confidence: " << confidence << std::endl;

    return 0;
}

#ifdef QRTOOL_MAIN
int main(int argc, char *argv[])
{
    string cmd = "help";
    if (argc > 1) {
        cmd = argv[1];
    }
    vector<string> cmds;

#define add_cmd(c, nargs) \
    do { \
        cmds.push_back(#c); \
        if (cmd == #c && argc >= 2 + nargs) return c##_cmd(&argv[2], argc - 2); \
    } while (0)

    add_cmd(detect, 1);
    add_cmd(angle, 1);
    add_cmd(dot, 1);
    add_cmd(clarity, 1);
    add_cmd(rectify, 1);
    add_cmd(topleft, 1);
    add_cmd(frame_roi, 1);
    add_cmd(roi, 1);
    add_cmd(roi_bench, 1);
#if USE_PULSAR
    add_cmd(roi_worker, 1);
    add_cmd(roi_worker_nop, 1);
#endif
#if ENABLE_GRPC
    add_cmd(grpc_server, 1);
#endif
    add_cmd(http_server, 1);
    add_cmd(verify, 2);
    add_cmd(roi_verify, 2);
    usage(argv[0], cmds);
    return 1;
}
#endif
