namespace PhpefRuntimeCore {
    public class EmbeddedResourceMetadata : Object {
        public string path { get; private set; }
        public uint8[] data { get; private set; }
        public size_t length { get { return (size_t) data.length; } }

        public EmbeddedResourceMetadata(string path, uint8[] data) {
            this.path = path;
            this.data = data;
        }
    }

    private HashTable<string, EmbeddedResourceMetadata>? embedded_resource_table = null;
    private bool embedded_resource_sections_loaded = false;
    private bool embedded_resource_sections_loading = false;

    private static void ensure_embedded_resource_registry() {
        if (embedded_resource_table == null) {
            embedded_resource_table = new HashTable<string, EmbeddedResourceMetadata>(str_hash, str_equal);
        }
        load_embedded_resource_sections();
    }

    private static bool normalize_embedded_resource_path(string resource_path, out string normalized_path) {
        normalized_path = "";
        if (resource_path == "") {
            return false;
        }
        if (!resource_path.has_prefix("/")) {
            return false;
        }
        if (resource_path.contains("://")) {
            return false;
        }
        normalized_path = resource_path;
        return true;
    }

    public static void register_embedded_resource(string resource_path, uint8[] data) throws Error {
        ensure_embedded_resource_registry();
        string normalized_path;
        if (!normalize_embedded_resource_path(resource_path, out normalized_path)) {
            throw_exception("Error", Zval.from_string("Invalid embedded resource path: " + resource_path));
        }
        if (embedded_resource_table != null && embedded_resource_table.lookup(normalized_path) != null) {
            throw_exception("Error", Zval.from_string("Cannot redeclare embedded resource " + normalized_path));
        }
        embedded_resource_table.insert(normalized_path, new EmbeddedResourceMetadata(normalized_path, data));
    }

    public static bool has_registered_embedded_resource(string resource_path) {
        return lookup_embedded_resource(resource_path) != null;
    }

    public static EmbeddedResourceMetadata? lookup_embedded_resource(string resource_path) {
        ensure_embedded_resource_registry();
        string normalized_path;
        if (!normalize_embedded_resource_path(resource_path, out normalized_path)) {
            return null;
        }
        return embedded_resource_table != null ? embedded_resource_table.lookup(normalized_path) : null;
    }

    private static void load_embedded_resource_sections() {
        if (embedded_resource_sections_loaded || embedded_resource_sections_loading) {
            return;
        }
        embedded_resource_sections_loaded = true;
        embedded_resource_sections_loading = true;

        foreach (var elf_path in embedded_resource_candidate_elf_paths()) {
            load_embedded_resource_sections_from_elf(elf_path);
        }

        embedded_resource_sections_loading = false;
    }

    private static void load_embedded_resource_sections_from_elf(string elf_path) {
        if (elf_path == "") {
            return;
        }

        uint8[] index_data;
        if (!resource_read_elf_section(elf_path, ".phpef_resource_index", out index_data)) {
            return;
        }

        if (resource_index_has_binary_magic(index_data)) {
            load_embedded_resource_binary_index(elf_path, index_data);
        }
        else {
            load_embedded_resource_text_index(elf_path, index_data);
        }
    }

    private static string[] embedded_resource_candidate_elf_paths() {
        string[] paths = {};

        string? executable_path = null;
        try { executable_path = FileUtils.read_link("/proc/self/exe"); } catch (FileError e) { executable_path = null; }
        if (executable_path != null && executable_path != "") {
            paths += executable_path;
        }

        string maps_text;
        try {
            FileUtils.get_contents("/proc/self/maps", out maps_text);
        } catch (Error e) {
            return paths;
        }

        foreach (var raw_line in maps_text.split("\n")) {
            var line = raw_line.strip();
            var path_offset = line.index_of("/");
            if (path_offset < 0) {
                continue;
            }
            var candidate = line.substring(path_offset).strip();
            if (candidate.has_suffix(" (deleted)")) {
                candidate = candidate.substring(0, candidate.length - 10);
            }
            if (candidate == "" || !candidate.has_prefix("/")) {
                continue;
            }
            if (!FileUtils.test(candidate, FileTest.IS_REGULAR)) {
                continue;
            }
            if (!resource_string_array_contains(paths, candidate)) {
                paths += candidate;
            }
        }

        return paths;
    }

    private static bool resource_string_array_contains(string[] values, string value) {
        foreach (var item in values) {
            if (item == value) {
                return true;
            }
        }
        return false;
    }

    private static bool resource_index_has_binary_magic(uint8[] data) {
        uint8[] magic = {80, 72, 80, 69, 70, 82, 69, 83, 73, 68, 88, 49, 0};
        if (data.length < magic.length + 4) {
            return false;
        }
        for (int i = 0; i < magic.length; i++) {
            if (data[i] != magic[i]) {
                return false;
            }
        }
        return true;
    }

    private static void load_embedded_resource_binary_index(string executable_path, uint8[] index_data) {
        uint64 offset = 13;
        if (!resource_range_available(index_data, offset, 4)) {
            return;
        }
        var count = resource_read_le32(index_data, offset);
        offset += 4;
        for (uint32 i = 0; i < count; i++) {
            if (!resource_range_available(index_data, offset, 2)) return;
            var path_len = resource_read_le16(index_data, offset);
            offset += 2;
            if (!resource_range_available(index_data, offset, path_len)) return;
            var resource_path = resource_bytes_to_string(index_data, offset, path_len);
            offset += path_len;

            if (!resource_range_available(index_data, offset, 2)) return;
            var section_len = resource_read_le16(index_data, offset);
            offset += 2;
            if (!resource_range_available(index_data, offset, section_len)) return;
            var section_name = resource_bytes_to_string(index_data, offset, section_len);
            offset += section_len;

            if (!resource_range_available(index_data, offset, 8 + 32)) return;
            var expected_size = resource_read_le64(index_data, offset);
            offset += 8;
            var expected_sha256 = resource_bytes_to_hex(index_data, offset, 32);
            offset += 32;

            load_embedded_resource_index_entry(executable_path, resource_path, section_name, expected_size, expected_sha256);
        }
    }

    private static void load_embedded_resource_text_index(string executable_path, uint8[] index_data) {
        var index_text = resource_bytes_to_text(index_data);
        foreach (var raw_line in index_text.split("\n")) {
            var line = raw_line.strip();
            if (line == "" || line.has_prefix("#")) {
                continue;
            }
            var fields = line.split("\t");
            if (fields.length < 4) {
                continue;
            }
            uint64 expected_size = 0;
            if (!uint64.try_parse(fields[2], out expected_size)) {
                continue;
            }
            load_embedded_resource_index_entry(executable_path, fields[0], fields[1], expected_size, fields[3].down());
        }
    }

    private static void load_embedded_resource_index_entry(string executable_path, string resource_path, string section_name, uint64 expected_size, string expected_sha256) {
        string normalized_path;
        if (!normalize_embedded_resource_path(resource_path, out normalized_path)) {
            return;
        }
        if (!section_name.has_prefix(".phpef_resource.")) {
            return;
        }
        if (embedded_resource_table != null && embedded_resource_table.lookup(normalized_path) != null) {
            return;
        }
        uint8[] resource_data;
        if (!resource_read_elf_section(executable_path, section_name, out resource_data)) {
            return;
        }
        if ((uint64) resource_data.length != expected_size) {
            return;
        }
        var checksum = new Checksum(ChecksumType.SHA256);
        checksum.update(resource_data, resource_data.length);
        var actual_sha256 = checksum.get_string();
        if (actual_sha256 == null || actual_sha256.down() != expected_sha256) {
            return;
        }
        embedded_resource_table.insert(normalized_path, new EmbeddedResourceMetadata(normalized_path, resource_data));
    }

    private static string resource_bytes_to_string(uint8[] data, uint64 offset, uint64 length) {
        var builder = new StringBuilder();
        if (!resource_range_available(data, offset, length)) return "";
        for (uint64 i = 0; i < length; i++) {
            builder.append_c((char) data[(int) (offset + i)]);
        }
        return builder.str;
    }

    private static string resource_bytes_to_hex(uint8[] data, uint64 offset, uint64 length) {
        var builder = new StringBuilder();
        if (!resource_range_available(data, offset, length)) return "";
        for (uint64 i = 0; i < length; i++) {
            builder.append_printf("%02x", data[(int) (offset + i)]);
        }
        return builder.str;
    }

    private static string resource_bytes_to_text(uint8[] data) {
        var builder = new StringBuilder();
        foreach (var b in data) {
            if (b == 0) break;
            builder.append_c((char) b);
        }
        return builder.str;
    }

    private static bool resource_read_elf_section(string path, string section_name, out uint8[] section_data) {
        section_data = {};
        uint8[] data;
        try { FileUtils.get_data(path, out data); } catch (Error e) { return false; }
        if (data.length < 64 || data[0] != 0x7f || data[1] != 'E' || data[2] != 'L' || data[3] != 'F') return false;
        if (data[4] != 2 || data[5] != 1) return false;
        var shoff = resource_read_le64(data, 40);
        var shentsize = resource_read_le16(data, 58);
        var shnum = resource_read_le16(data, 60);
        var shstrndx = resource_read_le16(data, 62);
        if (shoff == 0 || shentsize < 64 || shnum == 0 || shstrndx >= shnum) return false;
        var shstr_header = shoff + ((uint64) shstrndx * (uint64) shentsize);
        if (!resource_range_available(data, shstr_header, 64)) return false;
        var shstr_offset = resource_read_le64(data, shstr_header + 24);
        var shstr_size = resource_read_le64(data, shstr_header + 32);
        for (uint16 i = 0; i < shnum; i++) {
            var header = shoff + ((uint64) i * (uint64) shentsize);
            if (!resource_range_available(data, header, 64)) return false;
            var name_offset = resource_read_le32(data, header);
            var name = resource_read_null_string(data, shstr_offset + (uint64) name_offset, shstr_offset + shstr_size);
            if (name != section_name) continue;
            var section_offset = resource_read_le64(data, header + 24);
            var section_size = resource_read_le64(data, header + 32);
            if (!resource_range_available(data, section_offset, section_size)) return false;
            uint8[] out_data = {};
            for (uint64 n = 0; n < section_size; n++) {
                out_data += data[(int) (section_offset + n)];
            }
            section_data = out_data;
            return true;
        }
        return false;
    }

    private static bool resource_range_available(uint8[] data, uint64 offset, uint64 size) {
        return offset <= (uint64) data.length && size <= ((uint64) data.length - offset);
    }

    private static uint16 resource_read_le16(uint8[] data, uint64 offset) {
        if (!resource_range_available(data, offset, 2)) return 0;
        return (uint16) data[(int) offset] | ((uint16) data[(int) offset + 1] << 8);
    }

    private static uint32 resource_read_le32(uint8[] data, uint64 offset) {
        if (!resource_range_available(data, offset, 4)) return 0;
        return (uint32) data[(int) offset]
            | ((uint32) data[(int) offset + 1] << 8)
            | ((uint32) data[(int) offset + 2] << 16)
            | ((uint32) data[(int) offset + 3] << 24);
    }

    private static uint64 resource_read_le64(uint8[] data, uint64 offset) {
        if (!resource_range_available(data, offset, 8)) return 0;
        uint64 value = 0;
        for (int i = 7; i >= 0; i--) value = (value << 8) | data[(int) offset + i];
        return value;
    }

    private static string resource_read_null_string(uint8[] data, uint64 offset, uint64 limit) {
        var builder = new StringBuilder();
        if (offset >= (uint64) data.length) return "";
        if (limit > (uint64) data.length) limit = (uint64) data.length;
        for (uint64 i = offset; i < limit; i++) {
            var b = data[(int) i];
            if (b == 0) break;
            builder.append_c((char) b);
        }
        return builder.str;
    }
}
