/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.iceberg;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Paths;
import org.apache.iceberg.exceptions.AlreadyExistsException;
import org.apache.iceberg.exceptions.NotFoundException;
import org.apache.iceberg.exceptions.RuntimeIOException;
import org.apache.iceberg.io.InputFile;
import org.apache.iceberg.io.OutputFile;
import org.apache.iceberg.io.PositionOutputStream;
import org.apache.iceberg.io.SeekableInputStream;

public class Files {

  private Files() {}

  public static OutputFile localOutput(File file) {
    return new LocalOutputFile(file);
  }

  public static OutputFile localOutput(String file) {
    if (file.startsWith("file:")) {
      return localOutput(new File(file.replaceFirst("file:", "")));
    }
    return localOutput(Paths.get(file).toAbsolutePath().toFile());
  }

  private static class LocalOutputFile implements OutputFile {
    private final File file;

    private LocalOutputFile(File file) {
      this.file = file;
    }

    @Override
    public PositionOutputStream create() {
      if (file.exists()) {
        throw new AlreadyExistsException("File already exists: %s", file);
      }

      if (!file.getParentFile().isDirectory() && !file.getParentFile().mkdirs()) {
        throw new RuntimeIOException(
            "Failed to create the file's directory at %s.", file.getParentFile().getAbsolutePath());
      }

      try {
        return new PositionFileOutputStream(file, new RandomAccessFile(file, "rw"));
      } catch (FileNotFoundException e) {
        throw new NotFoundException(e, "Failed to create file: %s", file);
      }
    }

    @Override
    public PositionOutputStream createOrOverwrite() {
      if (file.exists()) {
        if (!file.delete()) {
          throw new RuntimeIOException("Failed to delete: %s", file);
        }
      }
      return create();
    }

    @Override
    public String location() {
      return file.toString();
    }

    @Override
    public InputFile toInputFile() {
      return localInput(file);
    }

    @Override
    public String toString() {
      return location();
    }
  }

  public static InputFile localInput(File file) {
    return new LocalInputFile(file);
  }

  public static InputFile localInput(String file) {
    if (file.startsWith("file:")) {
      return localInput(new File(file.replaceFirst("file:", "")));
    }
    return localInput(new File(file));
  }

  private static class LocalInputFile implements InputFile {
    private final File file;

    private LocalInputFile(File file) {
      this.file = file;
    }

    @Override
    public long getLength() {
      return file.length();
    }

    @Override
    public SeekableInputStream newStream() {
      try {
        return new SeekableFileInputStream(new RandomAccessFile(file, "r"));
      } catch (FileNotFoundException e) {
        throw new NotFoundException(e, "Failed to read file: %s", file);
      }
    }

    @Override
    public String location() {
      return file.toString();
    }

    @Override
    public boolean exists() {
      return file.exists();
    }

    @Override
    public String toString() {
      return location();
    }
  }

  private static class SeekableFileInputStream extends SeekableInputStream {
    private final RandomAccessFile stream;

    private SeekableFileInputStream(RandomAccessFile stream) {
      this.stream = stream;
    }

    @Override
    public long getPos() throws IOException {
      return stream.getFilePointer();
    }

    @Override
    public void seek(long newPos) throws IOException {
      stream.seek(newPos);
    }

    @Override
    public int read() throws IOException {
      return stream.read();
    }

    @Override
    public int read(byte[] b) throws IOException {
      return stream.read(b);
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
      return stream.read(b, off, len);
    }

    @Override
    public long skip(long n) throws IOException {
      if (n > Integer.MAX_VALUE) {
        return stream.skipBytes(Integer.MAX_VALUE);
      } else {
        return stream.skipBytes((int) n);
      }
    }

    @Override
    public void close() throws IOException {
      stream.close();
    }
  }

  private static class PositionFileOutputStream extends PositionOutputStream {
    private final File file;
    private final RandomAccessFile stream;
    private boolean isClosed = false;

    private PositionFileOutputStream(File file, RandomAccessFile stream) {
      this.file = file;
      this.stream = stream;
    }

    @Override
    public long getPos() throws IOException {
      if (isClosed) {
        return file.length();
      }
      return stream.getFilePointer();
    }

    @Override
    public void write(byte[] b) throws IOException {
      stream.write(b);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
      stream.write(b, off, len);
    }

    @Override
    public void write(int b) throws IOException {
      stream.write(b);
    }

    @Override
    public void close() throws IOException {
      stream.close();
      this.isClosed = true;
    }
  }
}
