import Socket from 'net';

import * as constants from '../utils/constants';

import packet from './packet';

export default class connection {
  constructor(host_or_ip, port, password, socket) {
    this.host_or_ip = host_or_ip;
    this.port = port;
    this.password = password;
    if (socket) {
      this.socket = socket;
      this.connected = true;
      this.setup_socket();
    }
  }

  connected = false;
  host_or_ip = '';
  password = '';
  port = 20000;
  reject;
  resolve;
  socket;

  cleanup_handlers() {
    this.reject = null;
    this.resolve = null;
  }

  async connect() {
    if (!this.socket) {
      try {
        await new Promise((resolve, reject) => {
          this.socket = Socket.createConnection(
            this.port,
            this.host_or_ip,
            (err) => {
              if (err) {
                console.log(err);
                return reject(err);
              }
              return resolve();
            }
          );
        });
      } catch (err) {
        return Promise.reject(new Error(`'connect()' failed: ${err}`));
      }

      this.connected = true;
      this.setup_socket();
    }
  }

  setup_socket() {
    let buffer;
    const cleanup = () => {
      this.cleanup_handlers();
      buffer = null;
    };

    this.socket.on('data', (chunk) => {
      buffer = buffer ? Buffer.concat([buffer, chunk]) : chunk;
      if (buffer.length > 4) {
        const size = buffer.readUInt32BE(0);
        if (buffer.length >= size + 4) {
          const packet_data = buffer.slice(4, 4 + size);
          if (this.resolve) {
            const complete = () => {
              const reject = this.reject;
              const resolve = this.resolve;
              cleanup();
              return {
                reject,
                resolve,
              };
            };

            const response = new packet(this.password);
            response.buffer = new Uint8Array(packet_data);
            response
              .decrypt()
              .then(() => {
                const { resolve } = complete();
                if (resolve) {
                  resolve(response);
                }
              })
              .catch((e) => {
                console.log(e);
                const { reject } = complete();
                if (reject) {
                  reject(e);
                }
              });
          }
        }
      }
    });

    this.socket.on('error', (e) => {
      if (this.reject) {
        const reject = this.reject;

        cleanup();

        this.connected = false;
        console.log(e);
        if (reject) {
          reject(e);
        }
      }
    });

    this.socket.on('close', () => {
      if (this.reject) {
        const reject = this.reject;

        cleanup();

        this.connected = false;
        console.log('socket closed');
        if (reject) {
          reject(new Error('socket closed'));
        }
      }
    });
  }

  async disconnect() {
    try {
      if (this.socket) {
        this.socket.destroy();
        this.socket = null;
        this.cleanup_handlers();
        this.connected = false;
      }
    } catch (e) {
      console.log(e);
    }
  }

  async send(method_name, packet, optional_thread_id) {
    packet.token = this.password;
    packet.encode_top_utf8(method_name);
    packet.encode_top_ui64(optional_thread_id || 1);
    packet.encode_top_utf8(constants.instance_id);
    packet.encode_top_ui32(0); // Service flags
    packet.encode_top_utf8(constants.get_version());
    await packet.encrypt();
    packet.encode_top_ui32(packet.buffer.length);
    return new Promise((resolve, reject) => {
      this.reject = reject;
      this.resolve = resolve;
      this.socket.write(Buffer.from(packet.buffer), null, (err) => {
        if (err) {
          this.cleanup_handlers();
          reject(err);
        }
      });
    });
  }
}
