const c = @import("c.zig");
const fs = @import("std").fs;
const std = @import("std");
const debug = @import("std").debug;

pub const OutputConfig = struct {
    const Type = enum {
        window,
    };

    const FilterMode = enum(c.GLuint) {
        nearest = c.GL_NEAREST,
        linear = c.GL_LINEAR,
    };

    type: Type,
    width: i32,
    height: i32,
    filter: FilterMode,

    // only for 'window'
    monitor: []const u8,
    fullscreen: bool,
};

pub const ParameterConfig = struct {
    name: []const u8,
    type: []const u8,
};

pub const OSCConfig = union(enum) {
    const Protocol = enum {
        udp,
        tcp,
        unix,
    };

    Manual: struct {
        protocol: Protocol = Protocol.udp,
        port: u16 = 9000,
    },
    URL: []const u8,
};

const defaultOutput = OutputConfig{
    .type = .window,
    .width = 800,
    .height = 600,
    .filter = .linear,
    .monitor = "",
    .fullscreen = false,
};

pub const Config = struct {
    width: i32,
    height: i32,
    outputs: []const OutputConfig,
    fragment: []const u8,
    parameters: []const ParameterConfig,
    project_root: fs.Dir,
    osc: OSCConfig,

    pub fn parse(allocator: *const std.mem.Allocator, filename: []const u8) !Config {
        var parser: c.yaml_parser_t = undefined;
        _ = c.yaml_parser_initialize(&parser);
        defer c.yaml_parser_delete(&parser);

        const file = try fs.cwd().openFile(filename, .{});
        var buffer: [1024]u8 = undefined;
        const len: usize = try file.read(buffer[0..]);
        c.yaml_parser_set_input_string(&parser, buffer[0..], len);

        const dirname = fs.path.dirname(filename) orelse ".";
        debug.print("file/dirname is {s} / {s}\n", .{ filename, dirname });
        var config: Config = .{
            .width = 1920,
            .height = 1080,
            .outputs = ([_]OutputConfig{defaultOutput})[0..],
            .fragment = "",
            .parameters = ([0]ParameterConfig{})[0..],
            .project_root = try fs.cwd().openDir(dirname, .{}),
            .osc = .{ .URL = "osc.udp://:9000" },
        };

        try expectEvent(&parser, c.YAML_STREAM_START_EVENT);
        try expectEvent(&parser, c.YAML_DOCUMENT_START_EVENT);
        try expectEvent(&parser, c.YAML_MAPPING_START_EVENT);

        while (true) {
            var event: c.yaml_event_t = undefined;
            if (c.yaml_parser_parse(&parser, &event) != 1)
                return error.YAMLParserError;
            defer c.yaml_event_delete(&event);

            switch (event.type) {
                c.YAML_MAPPING_END_EVENT => break,
                c.YAML_SCALAR_EVENT => {
                    const data = event.data.scalar;
                    const key: []const u8 = data.value[0..data.length];

                    if (std.mem.eql(u8, key, "width")) {
                        config.width = try parseInt(&parser, i32);
                    } else if (std.mem.eql(u8, key, "height")) {
                        config.height = try parseInt(&parser, i32);
                    } else if (std.mem.eql(u8, key, "fragment")) {
                        config.fragment = try parseString(&parser, allocator);
                    } else if (std.mem.eql(u8, key, "outputs")) {
                        config.outputs = try parseOutputs(&parser, allocator);
                    } else if (std.mem.eql(u8, key, "parameters")) {
                        config.parameters = try parseParameters(&parser, allocator);
                    } else if (std.mem.eql(u8, key, "project_root")) {
                        config.project_root = try fs.cwd().openDir(try parseString(&parser, allocator), .{});
                    } else if (std.mem.eql(u8, key, "osc")) {
                        config.osc = try parseOSC(&parser, allocator);
                    } else {
                        debug.print("unknown key: '{s}'\n", .{key});
                        try skipAny(&parser);
                    }
                },
                else => {
                    debug.print("unexpected event: {}\n", .{event.type});
                    return error.InvalidConfiguration;
                },
            }
        }

        try expectEvent(&parser, c.YAML_DOCUMENT_END_EVENT);
        try expectEvent(&parser, c.YAML_STREAM_END_EVENT);

        return config;
    }
};

fn expectEvent(parser: *c.yaml_parser_t, expectedType: c.yaml_event_type_t) !void {
    var event: c.yaml_event_t = undefined;

    if (c.yaml_parser_parse(parser, &event) != 1)
        return error.YAMLParserError;
    defer c.yaml_event_delete(&event);

    if (event.type != expectedType) {
        debug.print("unexpected event: {}\n", .{event.type});
        return error.InvalidConfiguration;
    }
}

fn parseStringEvent(event: *c.yaml_event_t, allocator: *const std.mem.Allocator) ![]const u8 {
    if (event.type != c.YAML_SCALAR_EVENT)
        return error.InvalidType;

    const data = event.data.scalar;
    const value: []const u8 = data.value[0..data.length];
    return try allocator.dupe(u8, value);
}

fn parseString(parser: *c.yaml_parser_t, allocator: *const std.mem.Allocator) ![]const u8 {
    var event: c.yaml_event_t = undefined;
    if (c.yaml_parser_parse(parser, &event) != 1)
        return error.YAMLParserError;
    defer c.yaml_event_delete(&event);

    return parseStringEvent(&event, allocator);
}

fn parseInt(parser: *c.yaml_parser_t, comptime T: type) !T {
    var event: c.yaml_event_t = undefined;
    if (c.yaml_parser_parse(parser, &event) != 1)
        return error.YAMLParserError;
    defer c.yaml_event_delete(&event);

    if (event.type != c.YAML_SCALAR_EVENT)
        return error.InvalidType;

    const data = event.data.scalar;
    const value: []const u8 = data.value[0..data.length];
    return try std.fmt.parseInt(T, value, 10);
}

fn parseEnum(parser: *c.yaml_parser_t, comptime T: type) !T {
    var event: c.yaml_event_t = undefined;
    if (c.yaml_parser_parse(parser, &event) != 1)
        return error.YAMLParserError;
    defer c.yaml_event_delete(&event);

    if (event.type != c.YAML_SCALAR_EVENT)
        return error.InvalidType;

    const data = event.data.scalar;
    const value: []const u8 = data.value[0..data.length];

    inline for (@typeInfo(T).@"enum".fields) |field| {
        if (std.mem.eql(u8, value, field.name))
            return @field(T, field.name);
    }

    return error.InvalidValue;
}

fn parseBool(parser: *c.yaml_parser_t) !bool {
    var event: c.yaml_event_t = undefined;
    if (c.yaml_parser_parse(parser, &event) != 1)
        return error.YAMLParserError;
    defer c.yaml_event_delete(&event);

    if (event.type != c.YAML_SCALAR_EVENT)
        return error.InvalidType;

    const data = event.data.scalar;
    const value: []const u8 = data.value[0..data.length];

    if (data.style != c.YAML_PLAIN_SCALAR_STYLE)
        return false;
    return std.mem.eql(u8, value, "true") or std.mem.eql(u8, value, "1");
}

fn parseOutputs(parser: *c.yaml_parser_t, allocator: *const std.mem.Allocator) ![]OutputConfig {
    try expectEvent(parser, c.YAML_SEQUENCE_START_EVENT);

    var outputs = try allocator.alloc(OutputConfig, 1024);
    var output_count: usize = 0;

    while (true) {
        var seqEvent: c.yaml_event_t = undefined;
        if (c.yaml_parser_parse(parser, &seqEvent) != 1)
            return error.YAMLParserError;
        defer c.yaml_event_delete(&seqEvent);

        switch (seqEvent.type) {
            c.YAML_SEQUENCE_END_EVENT => break,
            c.YAML_MAPPING_START_EVENT => {
                const output = &outputs[output_count];
                output.* = defaultOutput;

                while (true) {
                    var event: c.yaml_event_t = undefined;
                    if (c.yaml_parser_parse(parser, &event) != 1)
                        return error.YAMLParserError;
                    defer c.yaml_event_delete(&event);

                    switch (event.type) {
                        c.YAML_MAPPING_END_EVENT => break,
                        c.YAML_SCALAR_EVENT => {
                            const data = event.data.scalar;
                            const key: []const u8 = data.value[0..data.length];

                            if (std.mem.eql(u8, key, "type")) {
                                output.*.type = try parseEnum(parser, OutputConfig.Type);
                            } else if (std.mem.eql(u8, key, "width")) {
                                output.*.width = try parseInt(parser, i32);
                            } else if (std.mem.eql(u8, key, "height")) {
                                output.*.height = try parseInt(parser, i32);
                            } else if (std.mem.eql(u8, key, "filter")) {
                                output.*.filter = try parseEnum(parser, OutputConfig.FilterMode);
                            } else if (std.mem.eql(u8, key, "monitor")) {
                                output.*.monitor = try parseString(parser, allocator);
                            } else if (std.mem.eql(u8, key, "fullscreen")) {
                                output.*.fullscreen = try parseBool(parser);
                            } else {
                                debug.print("unknown key: '{s}'\n", .{key});
                                try skipAny(parser);
                            }
                        },
                        else => {
                            debug.print("unexpected event: {}\n", .{event.type});
                            return error.InvalidConfiguration;
                        },
                    }
                }

                output_count += 1;
            },
            else => {
                debug.print("unexpected event: {}\n", .{seqEvent.type});
                return error.InvalidConfiguration;
            },
        }
    }

    _ = allocator.realloc(outputs, output_count) catch 0;
    return outputs[0..output_count];
}

fn parseParameters(parser: *c.yaml_parser_t, allocator: *const std.mem.Allocator) ![]ParameterConfig {
    try expectEvent(parser, c.YAML_SEQUENCE_START_EVENT);

    var parameters = try allocator.alloc(ParameterConfig, 1024);
    var param_count: usize = 0;

    while (true) {
        var seqEvent: c.yaml_event_t = undefined;
        if (c.yaml_parser_parse(parser, &seqEvent) != 1)
            return error.YAMLParserError;
        defer c.yaml_event_delete(&seqEvent);

        switch (seqEvent.type) {
            c.YAML_SEQUENCE_END_EVENT => break,
            c.YAML_MAPPING_START_EVENT => {
                const param = &parameters[param_count];
                var have_name = false;
                var have_type = false;

                while (true) {
                    var event: c.yaml_event_t = undefined;
                    if (c.yaml_parser_parse(parser, &event) != 1)
                        return error.YAMLParserError;
                    defer c.yaml_event_delete(&event);

                    switch (event.type) {
                        c.YAML_MAPPING_END_EVENT => break,
                        c.YAML_SCALAR_EVENT => {
                            const data = event.data.scalar;
                            const key: []const u8 = data.value[0..data.length];

                            if (std.mem.eql(u8, key, "name")) {
                                param.*.name = try parseString(parser, allocator);
                                have_name = true;
                            } else if (std.mem.eql(u8, key, "type")) {
                                param.*.type = try parseString(parser, allocator);
                                have_type = true;
                            } else {
                                debug.print("unknown key: '{s}'\n", .{key});
                                try skipAny(parser);
                            }
                        },
                        else => {
                            debug.print("unexpected event: {}\n", .{event.type});
                            return error.InvalidConfiguration;
                        },
                    }
                }

                if (!(have_name and have_type)) {
                    debug.print("name and type are mandatory for parameters.\n", .{});
                    return error.InvalidConfiguration;
                }

                param_count += 1;
            },
            else => {
                debug.print("unexpected event: {}\n", .{seqEvent.type});
                return error.InvalidConfiguration;
            },
        }
    }

    _ = allocator.realloc(parameters, param_count) catch 0;
    return parameters[0..param_count];
}

fn parseOSC(parser: *c.yaml_parser_t, allocator: *const std.mem.Allocator) !OSCConfig {
    var event: c.yaml_event_t = undefined;
    if (c.yaml_parser_parse(parser, &event) != 1)
        return error.YAMLParserError;
    defer c.yaml_event_delete(&event);

    if (event.type == c.YAML_SCALAR_EVENT) {
        const url = try parseStringEvent(&event, allocator);
        return OSCConfig{ .URL = url };
    } else if (event.type != c.YAML_MAPPING_START_EVENT) {
        debug.print("unexpected event: {}\n", .{event.type});
        return error.InvalidConfiguration;
    }

    var config: OSCConfig = .{ .Manual = .{} };

    while (true) {
        if (c.yaml_parser_parse(parser, &event) != 1)
            return error.YAMLParserError;
        defer c.yaml_event_delete(&event);

        switch (event.type) {
            c.YAML_MAPPING_END_EVENT => break,
            c.YAML_SCALAR_EVENT => {
                const data = event.data.scalar;
                const key: []const u8 = data.value[0..data.length];

                if (std.mem.eql(u8, key, "protocol")) {
                    config.Manual.protocol = try parseEnum(parser, OSCConfig.Protocol);
                } else if (std.mem.eql(u8, key, "port")) {
                    config.Manual.port = try parseInt(parser, u16);
                } else {
                    debug.print("unknown key: '{s}'\n", .{key});
                    try skipAny(parser);
                }
            },
            else => {
                debug.print("unexpected event: {}\n", .{event.type});
                return error.InvalidConfiguration;
            },
        }
    }

    return config;
}

fn skipAny(parser: *c.yaml_parser_t) !void {
    var event: c.yaml_event_t = undefined;
    var depth: u32 = 0;
    while (true) {
        if (c.yaml_parser_parse(parser, &event) != 1)
            return error.YAMLParserError;
        defer c.yaml_event_delete(&event);

        switch (event.type) {
            c.YAML_SCALAR_EVENT => {
                if (depth == 0)
                    break;
            },

            c.YAML_SEQUENCE_START_EVENT,
            c.YAML_MAPPING_START_EVENT,
            => depth += 1,

            c.YAML_SEQUENCE_END_EVENT,
            c.YAML_MAPPING_END_EVENT,
            => {
                depth -= 1;

                if (depth == 0)
                    break;
            },

            else => {
                debug.print("unexpected event: {}\n", .{event.type});
                return error.InvalidConfiguration;
            },
        }
    }
}
