import fs from 'fs';
import { Uint64BE } from 'int64-buffer';

import file from '../io/file';
import packet from '../networking/packet';

const _snapshot_directory = async (conn, remote_path) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);

    const response = await conn.send(
      '::RemoteJSONCreateDirectorySnapshot',
      request
    );
    response.decode_ui32(); // Service flags

    const result = response.decode_i32();
    if (result === 0) {
      const data = JSON.parse(response.decode_utf8());

      let released = false;
      const release = async () => {
        if (!released) {
          released = true;
          const request = new packet();
          request.encode_ui64(data.handle);
          await conn.send('::RemoteJSONReleaseDirectorySnapshot', request);
        }
      };

      try {
        const get_page = async (page) => {
          try {
            const request = new packet();
            request.encode_utf8(remote_path);
            request.encode_ui64(data.handle);
            request.encode_ui32(page);

            const response = await conn.send(
              '::RemoteJSONReadDirectorySnapshot',
              request
            );
            response.decode_ui32(); // Service flags

            const result = response.decode_i32();
            if (result === 0 || result === -120) {
              const data = JSON.parse(response.decode_utf8());
              return data.directory_list;
            }
          } catch (err) {
            await release();
            return Promise.reject(new Error(`'get_page' failed: ${err}`));
          }
          return [];
        };

        return {
          get_page,
          page_count: data.page_count,
          release,
          remote_path,
        };
      } catch (err) {
        await release();
        return Promise.reject(new Error(`'snapshot_directory' failed: ${err}`));
      }
    }
  } catch (err) {
    return Promise.reject(new Error(`'snapshot_directory' failed: ${err}`));
  }
};

export const close_file = async (
  conn,
  remote_path,
  handle,
  optional_thread_id
) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);
    request.encode_ui64(handle);

    const response = await conn.send(
      '::RemoteFUSERelease',
      request,
      optional_thread_id
    );
    response.decode_ui32(); // Service flags

    return response.decode_i32();
  } catch (err) {
    return Promise.reject(new Error(`'close_file' failed: ${err}`));
  }
};

export const create_directory = async (conn, remote_path) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);
    request.encode_ui16((7 << 6) | (5 << 3));

    const response = await conn.send('::RemoteFUSEMkdir', request);
    response.decode_ui32(); // Service flags

    return response.decode_i32();
  } catch (err) {
    return Promise.reject(new Error(`'create_directory' failed: ${err}`));
  }
};

export const create_or_open_file = async (
  conn,
  remote_path,
  optional_thread_id
) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);
    request.encode_ui16((7 << 6) | (5 << 3));
    request.encode_ui32(2 | 4); // Read-Write, Create

    const response = await conn.send(
      '::RemoteFUSECreate',
      request,
      optional_thread_id
    );
    response.decode_ui32(); // Service flags

    const result = response.decode_i32();
    if (result === 0) {
      return response.decode_ui64();
    }

    return Promise.reject(new Error(`'create_or_open_file' error: ${result}`));
  } catch (err) {
    return Promise.reject(new Error(`'create_or_open_file' failed: ${err}`));
  }
};

export const delete_file = async (conn, remote_path) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);

    const response = await conn.send('::RemoteFUSEUnlink', request);
    response.decode_ui32(); // Service flags

    return response.decode_i32();
  } catch (err) {
    return Promise.reject(new Error(`'delete_file' failed: ${err}`));
  }
};

export const download_file = async (
  conn,
  remote_path,
  local_path,
  progress_cb,
  overwrite,
  resume
) => {
  try {
    const src = new file(conn, await open_file(conn, remote_path), remote_path);
    const cleanup = async (fd) => {
      try {
        await src.close();
      } catch (err) {
        console.log(err);
      }
      try {
        if (fd !== undefined) {
          fs.closeSync(fd);
        }
      } catch (err) {
        console.log(err);
      }
    };

    try {
      const src_size = await src.get_size();
      let dst_fd;

      try {
        let offset = 0;
        if (overwrite) {
          dst_fd = fs.openSync(local_path, 'w+');
        } else if (resume) {
          dst_fd = fs.openSync(local_path, 'r+');

          const dst_size = fs.fstatSync(dst_fd).size;
          if (dst_size === src_size) {
            await cleanup(dst_fd);
            return true;
          }

          if (dst_size > src_size) {
            await cleanup(dst_fd);
            return Promise.reject(
              new Error(
                `'download_file' failed: destination is larger than source`
              )
            );
          }

          offset = dst_size;
        } else {
          if (fs.existsSync(local_path)) {
            await cleanup(dst_fd);
            return Promise.reject(
              new Error(`'download_file' failed: file exists`)
            );
          }

          dst_fd = fs.openSync(local_path, 'wx+');
        }

        let remain = src_size - offset;
        while (remain > 0) {
          const to_write = remain >= 65536 ? 65536 : remain;
          const buffer = await src.read(offset, to_write);
          const written = fs.writeSync(dst_fd, buffer, 0, to_write, offset);
          if (written > 0) {
            remain -= written;
            offset += written;
            if (progress_cb) {
              progress_cb(
                local_path,
                remote_path,
                ((src_size - remain) / src_size) * 100.0,
                false
              );
            }
          }
        }

        if (progress_cb) {
          progress_cb(local_path, remote_path, 100, true);
        }

        await cleanup(dst_fd);
        return true;
      } catch (err) {
        await cleanup(dst_fd);
        return Promise.reject(new Error(`'download_file' failed: ${err}`));
      }
    } catch (err) {
      await cleanup();
      return Promise.reject(new Error(`'download_file' failed: ${err}`));
    }
  } catch (err) {
    return Promise.reject(new Error(`'download_file' failed: ${err}`));
  }
};

export const get_drive_information = async (conn) => {
  try {
    const response = await conn.send(
      '::RemoteWinFSPGetVolumeInfo',
      new packet()
    );
    response.decode_ui32(); // Service flags

    const result = response.decode_i32();
    if (result === 0) {
      const total = response.decode_ui64();
      const free = response.decode_ui64();
      return {
        free,
        total,
        used: (new Uint64BE(total) - new Uint64BE(free)).toString(10),
      };
    }

    return Promise.reject(
      new Error(`'get_drive_information' failed: ${result}`)
    );
  } catch (err) {
    return Promise.reject(new Error(`'get_drive_information' failed: ${err}`));
  }
};

export const get_file_attributes = async (
  conn,
  handle,
  remote_path,
  optional_thread_id
) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);
    request.encode_ui64(handle);
    request.encode_ui32(0);
    request.encode_ui32(0);

    const response = await conn.send(
      '::RemoteFUSEFgetattr',
      request,
      optional_thread_id
    );
    response.decode_ui32(); // Service flags

    const result = response.decode_i32();
    if (result === 0) {
      return response.decode_stat();
    }

    return Promise.reject(new Error(`'get_file_attributes' failed: ${result}`));
  } catch (err) {
    return Promise.reject(new Error(`'get_file_attributes' failed: ${err}`));
  }
};

export const get_file_attributes2 = async (
  conn,
  remote_path,
  optional_thread_id
) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);
    request.encode_ui32(0);
    request.encode_ui32(0);

    const response = await conn.send(
      '::RemoteFUSEGetattr',
      request,
      optional_thread_id
    );
    response.decode_ui32(); // Service flags

    const result = response.decode_i32();
    if (result === 0) {
      return response.decode_stat();
    }

    return Promise.reject(
      new Error(`'get_file_attributes2' failed: ${result}`)
    );
  } catch (err) {
    return Promise.reject(new Error(`'get_file_attributes2' failed: ${err}`));
  }
};

export const list_directory = async (conn, remote_path, page_reader_cb) => {
  const dir_snapshot = await _snapshot_directory(conn, remote_path);
  try {
    await page_reader_cb(
      dir_snapshot.remote_path,
      dir_snapshot.page_count,
      dir_snapshot.get_page
    );
    await dir_snapshot.release();
  } catch (err) {
    await dir_snapshot.release();
    return Promise.reject(`'list_directory' failed: ${err}`);
  }
};

export const open_file = async (conn, remote_path, optional_thread_id) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);
    request.encode_ui32(2); // Read-Write

    const response = await conn.send(
      '::RemoteFUSEOpen',
      request,
      optional_thread_id
    );
    response.decode_ui32(); // Service flags

    const result = response.decode_i32();
    if (result === 0) {
      return response.decode_ui64();
    }
    return Promise.reject(new Error(`'open_file' error: ${result}`));
  } catch (err) {
    return Promise.reject(new Error(`'open_file' failed: ${err}`));
  }
};

export const read_file = async (
  conn,
  handle,
  remote_path,
  offset,
  length,
  optional_thread_id
) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);
    request.encode_ui64(length);
    request.encode_ui64(offset);
    request.encode_ui64(handle);

    const response = await conn.send(
      '::RemoteFUSERead',
      request,
      optional_thread_id
    );
    response.decode_ui32(); // Service flags

    const result = response.decode_i32();
    if (result === length) {
      return response.decode_buffer(result);
    }
    return Promise.reject(new Error(`'read_file' error: ${result}`));
  } catch (err) {
    return Promise.reject(new Error(`'read_file' failed: ${err}`));
  }
};

export const remove_directory = async (conn, remote_path) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);

    const response = await conn.send('::RemoteFUSERmdir', request);
    response.decode_ui32(); // Service flags

    return response.decode_i32();
  } catch (err) {
    return Promise.reject(new Error(`'remove_directory' failed: ${err}`));
  }
};

export const snapshot_directory = _snapshot_directory;

export const truncate_file = async (
  conn,
  handle,
  remote_path,
  length,
  optional_thread_id
) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);
    request.encode_ui64(length);
    request.encode_ui64(handle);

    const response = await conn.send(
      '::RemoteFUSEFtruncate',
      request,
      optional_thread_id
    );
    response.decode_ui32(); // Service flags

    return response.decode_i32();
  } catch (err) {
    return Promise.reject(new Error(`'truncate_file' failed: ${err}`));
  }
};

export const upload_file = async (
  conn,
  local_path,
  remote_path,
  progress_cb,
  overwrite,
  resume
) => {
  try {
    const src_fd = fs.openSync(local_path, 'r');
    const cleanup = async (f) => {
      try {
        fs.closeSync(src_fd);
      } catch (err) {
        console.log(err);
      }
      try {
        if (f) {
          await f.close();
        }
      } catch (err) {
        console.log(err);
      }
    };
    try {
      const src_st = fs.fstatSync(src_fd);
      let dst;
      const create_dest = async () => {
        dst = new file(
          conn,
          await create_or_open_file(conn, remote_path),
          remote_path
        );
      };

      try {
        let offset = 0;
        if (overwrite) {
          await create_dest();
          const result = await dst.truncate(0);
          if (result !== 0) {
            await cleanup(dst);
            return Promise.reject(new Error(`'upload_file' failed: ${result}`));
          }
        } else if (resume) {
          await create_dest();
          const dst_size = new Uint64BE(await dst.get_size()).toNumber();
          if (dst_size === src_st.size) {
            await cleanup(dst);
            return true;
          }

          if (dst_size > src_st.size) {
            await cleanup(dst);
            return Promise.reject(
              new Error(
                `'upload_file' failed: destination is larger than source`
              )
            );
          }

          offset = dst_size;
        } else {
          try {
            const f = new file(
              conn,
              await open_file(conn, remote_path),
              remote_path
            );
            await cleanup(f);
            return Promise.reject(
              new Error("'upload_file' failed: file exists")
            );
          } catch (err) {
            await create_dest();
          }
        }

        let remain = src_st.size - offset;
        const default_buffer = Buffer.alloc(65536 * 2);
        while (remain > 0) {
          const to_write =
            remain >= default_buffer.length ? default_buffer.length : remain;
          const buffer =
            to_write === default_buffer.length
              ? default_buffer
              : Buffer.alloc(to_write);
          fs.readSync(src_fd, buffer, 0, to_write, offset);
          const written = await dst.write(offset, buffer);
          if (written > 0) {
            remain -= written;
            offset += written;
            if (progress_cb) {
              progress_cb(
                local_path,
                remote_path,
                ((src_st.size - remain) / src_st.size) * 100.0,
                false
              );
            }
          }
        }

        if (progress_cb) {
          progress_cb(local_path, remote_path, 100, true);
        }

        await cleanup(dst);
        return true;
      } catch (err) {
        await cleanup(dst);
        return Promise.reject(new Error(`'upload_file' failed: ${err}`));
      }
    } catch (err) {
      await cleanup();
      return Promise.reject(new Error(`'upload_file' failed: ${err}`));
    }
  } catch (err) {
    return Promise.reject(new Error(`'upload_file' failed: ${err}`));
  }
};

export const write_file = async (
  conn,
  handle,
  remote_path,
  offset,
  buffer,
  optional_thread_id
) => {
  try {
    const request = new packet();
    request.encode_utf8(remote_path);
    request.encode_ui64(buffer.length);
    request.encode_buffer(buffer);
    request.encode_ui64(offset);
    request.encode_ui64(handle);

    const response = await conn.send(
      '::RemoteFUSEWrite',
      request,
      optional_thread_id
    );
    response.decode_ui32(); // Service flags

    const result = response.decode_i32();
    if (result === buffer.length) {
      return result;
    }
    return Promise.reject(new Error(`'write_file' error: ${result}`));
  } catch (err) {
    return Promise.reject(new Error(`'write_file' failed: ${err}`));
  }
};
