using Php;

namespace PhpNative {
    private errordomain StreamRuntimeError {
        RUNTIME
    }

    public class StreamResource : Object {
        private IOChannel? channel;
        private SocketConnection? connection;
        private bool close_channel;
        private bool closed;

        internal StreamResource (IOChannel channel, SocketConnection? connection = null, bool close_channel = true) {
            this.channel = channel;
            this.connection = connection;
            this.close_channel = close_channel;
            this.closed = false;
            try {
                this.channel.set_encoding (null);
            } catch (Error e) {
            }
            this.channel.set_buffered (false);
        }

        ~StreamResource () {
            close ();
        }

        public bool is_closed () {
            return closed || channel == null;
        }

        public bool is_resource () {
            return !is_closed ();
        }

        internal IOChannel get_channel () throws Error {
            if (channel == null || closed) {
                throw new StreamRuntimeError.RUNTIME ("stream is closed");
            }
            return channel;
        }

        public void close () {
            if (closed) {
                return;
            }
            closed = true;
            if (channel != null) {
                try {
                    if (close_channel) {
                        channel.shutdown (true);
                    } else {
                        channel.flush ();
                    }
                } catch (Error e) {
                }
                channel = null;
            }
            if (connection != null) {
                try {
                    connection.close ();
                } catch (Error e) {
                }
                connection = null;
            }
        }
    }

    private void throw_stream_error (string function_name, string php_file, int php_line, string message) throws Error {
        throw new StreamRuntimeError.RUNTIME ("%s:%d: PHP %s failed: %s", php_file, php_line, function_name, message);
    }

    private StreamResource zval_to_stream_resource (zval value, string function_name, string php_file, int php_line) throws Error {
        StreamResource? stream = value != null ? value.as_resource () as StreamResource : null;
        if (stream == null) {
            throw_stream_error (function_name, php_file, php_line, "not a stream resource");
        }
        return stream;
    }

    public StreamResource fopen_static (string path, string mode, string php_file, int php_line) throws Error {
        try {
            if (path == "php://stdout") {
                return new StreamResource (new IOChannel.unix_new (1), null, false);
            }
            if (path == "php://stderr") {
                return new StreamResource (new IOChannel.unix_new (2), null, false);
            }
            if (path == "php://stdin") {
                return new StreamResource (new IOChannel.unix_new (0), null, false);
            }
            return new StreamResource (new IOChannel.file (path, mode));
        } catch (FileError e) {
            throw_stream_error ("fopen", php_file, php_line, e.message);
        }
        return new StreamResource (new IOChannel.unix_new (-1));
    }

    public zval fopen_zval (zval path, zval mode, string php_file, int php_line) throws Error {
        return zval.resource (fopen_static (zval_to_string (path), zval_to_string (mode), php_file, php_line));
    }

    public StreamResource stream_socket_client_static (string target, out int err_no, out string err_str, double timeout, int flags, string php_file, int php_line) throws Error {
        err_no = 0;
        err_str = "";
        try {
            string normalized = target;
            if (normalized.has_prefix ("tcp://")) {
                normalized = normalized.substring (6);
            }
            string[] parts = normalized.split (":", 2);
            if (parts.length != 2) {
                err_no = 1;
                err_str = "invalid target";
                throw_stream_error ("stream_socket_client", php_file, php_line, err_str);
            }

            var client = new SocketClient ();
            if (timeout > 0) {
                uint timeout_seconds = (uint) timeout;
                if ((double) timeout_seconds < timeout) {
                    timeout_seconds++;
                }
                client.timeout = timeout_seconds;
            }
            SocketConnection connection = client.connect_to_host (parts[0], (uint16) int.parse (parts[1]));
            int fd = connection.get_socket ().get_fd ();
            return new StreamResource (new IOChannel.unix_new (fd), connection);
        } catch (Error e) {
            err_no = 1;
            err_str = e.message;
            throw_stream_error ("stream_socket_client", php_file, php_line, e.message);
        }
        return new StreamResource (new IOChannel.unix_new (-1));
    }

    public zval stream_socket_client_zval (zval target, out zval err_no_value, out zval err_str_value, zval timeout, zval flags, string php_file, int php_line) throws Error {
        int err_no;
        string err_str;
        StreamResource stream = stream_socket_client_static (zval_to_string (target), out err_no, out err_str, zval_to_double (timeout), zval_to_int (flags), php_file, php_line);
        err_no_value = zval.integer (err_no);
        err_str_value = zval.string_value (err_str);
        return zval.resource (stream);
    }

    public bool fclose_static (StreamResource stream, string php_file, int php_line) throws Error {
        if (stream != null) {
            stream.close ();
        }
        return true;
    }

    public zval fclose_zval (zval stream, string php_file, int php_line) throws Error {
        return zval.boolean (fclose_static (zval_to_stream_resource (stream, "fclose", php_file, php_line), php_file, php_line));
    }

    public bool stream_set_blocking_static (StreamResource stream, bool enable, string php_file, int php_line) throws Error {
        IOChannel channel = stream.get_channel ();
        IOFlags flags = channel.get_flags ();
        if (enable) {
            flags &= ~IOFlags.NONBLOCK;
        } else {
            flags |= IOFlags.NONBLOCK;
        }
        channel.set_flags (flags);
        return true;
    }

    public zval stream_set_blocking_zval (zval stream, zval enable, string php_file, int php_line) throws Error {
        return zval.boolean (stream_set_blocking_static (zval_to_stream_resource (stream, "stream_set_blocking", php_file, php_line), truthy (enable), php_file, php_line));
    }

    public int stream_select_read_static (StreamResource stream, int seconds, int microseconds, string php_file, int php_line) throws Error {
        if (stream == null || !stream.is_resource ()) {
            return 0;
        }

        try {
            PollFD[] fds = new PollFD[1];
            fds[0].fd = stream.get_channel ().unix_get_fd ();
            fds[0].events = IOCondition.IN | IOCondition.HUP | IOCondition.ERR;
            fds[0].revents = 0;

            int timeout_ms = seconds * 1000 + microseconds / 1000;
            int result = GLib.poll (fds, timeout_ms);
            if (result <= 0) {
                return 0;
            }
            if ((fds[0].revents & IOCondition.IN) != 0 || (fds[0].revents & IOCondition.HUP) != 0) {
                return 1;
            }
            return 0;
        } catch (Error e) {
            throw_stream_error ("stream_select", php_file, php_line, e.message);
        }
        return 0;
    }

    public zval stream_select_zval (zval read, zval write, zval except, zval seconds, zval microseconds, string php_file, int php_line) throws Error {
        zval first = array_get (read, zval.integer (0));
        return zval.integer (stream_select_read_static (zval_to_stream_resource (first, "stream_select", php_file, php_line), zval_to_int (seconds), zval_to_int (microseconds), php_file, php_line));
    }

    public string fread_static (StreamResource stream, int length, string php_file, int php_line) throws Error {
        try {
            char[] buffer = new char[length];
            size_t bytes_read = 0;
            IOStatus status = stream.get_channel ().read_chars (buffer, out bytes_read);
            if (status == IOStatus.EOF || status == IOStatus.AGAIN || bytes_read == 0) {
                return "";
            }

            var result = new StringBuilder ();
            for (int i = 0; i < (int) bytes_read; i++) {
                result.append_c (buffer[i]);
            }
            return result.str;
        } catch (Error e) {
            throw_stream_error ("fread", php_file, php_line, e.message);
        }
        return "";
    }

    public zval fread_zval (zval stream, zval length, string php_file, int php_line) throws Error {
        return zval.string_value (fread_static (zval_to_stream_resource (stream, "fread", php_file, php_line), zval_to_int (length), php_file, php_line));
    }

    public int fwrite_static (StreamResource stream, string data, string php_file, int php_line) throws Error {
        try {
            char[] buffer = new char[data.length];
            for (int i = 0; i < data.length; i++) {
                buffer[i] = data[i];
            }
            size_t bytes_written = 0;
            stream.get_channel ().write_chars (buffer, out bytes_written);
            return (int) bytes_written;
        } catch (Error e) {
            throw_stream_error ("fwrite", php_file, php_line, e.message);
        }
        return 0;
    }

    public zval fwrite_zval (zval stream, zval data, string php_file, int php_line) throws Error {
        return zval.integer (fwrite_static (zval_to_stream_resource (stream, "fwrite", php_file, php_line), zval_to_string (data), php_file, php_line));
    }

    public bool fflush_static (StreamResource stream, string php_file, int php_line) throws Error {
        try {
            stream.get_channel ().flush ();
            return true;
        } catch (Error e) {
            throw_stream_error ("fflush", php_file, php_line, e.message);
        }
        return false;
    }

    public zval fflush_zval (zval stream, string php_file, int php_line) throws Error {
        return zval.boolean (fflush_static (zval_to_stream_resource (stream, "fflush", php_file, php_line), php_file, php_line));
    }

    public bool is_resource_static (StreamResource stream, string php_file, int php_line) throws Error {
        return stream != null && stream.is_resource ();
    }

    public zval is_resource_zval (zval stream, string php_file, int php_line) throws Error {
        StreamResource? resource = stream != null ? stream.as_resource () as StreamResource : null;
        return zval.boolean (resource != null && is_resource_static (resource, php_file, php_line));
    }
}
