/*
** 2011 April 5
**
** The author disclaims copyright to this source code.  In place of
** a legal notice, here is a blessing:
**    May you do good and not evil.
**    May you find forgiveness for yourself and forgive others.
**    May you share freely, never taking more than you give.
*/

package info.ata4.bspsrc;

import info.ata4.bsplib.entity.Entity;
import info.ata4.bsplib.entity.KeyValue;
import info.ata4.bsplib.vector.Vector3f;
import info.ata4.bspsrc.modules.texture.Texture;
import info.ata4.bspsrc.modules.texture.TextureAxis;
import info.ata4.log.LogUtils;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Collections;
import java.util.EmptyStackException;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Class to write formatted VMF files.
 *
 * @author Nico Bergemann <barracuda415 at yahoo.de>
 */
public class VmfWriter implements Closeable {

    private static final Logger L = LogUtils.getLogger();

    private final PrintWriter pw;
    private final Stack<String> section = new Stack<>();
    private final DecimalFormat decimalFormat = new DecimalFormat("0.####", new DecimalFormatSymbols(Locale.ENGLISH));

    public VmfWriter(File file) throws FileNotFoundException, UnsupportedEncodingException {
        pw = new PrintWriter(file, "US-ASCII");
    }

    public VmfWriter(OutputStream os) {
        pw = new PrintWriter(os);
    }

    private void indent() {
        for (int i = 0; i < section.size(); i++) {
            pw.print("\t");
        }
    }

    public void start(String name) {
        indent();
        pw.print(name);
        pw.print("\r\n");
        indent();
        pw.print("{\r\n");

        section.push(name);
    }

    public void end(String name) {
        try {
            if (!section.peek().equals(name)) {
                throw new IllegalArgumentException("VMF section end name mismatch: " + name
                        + ", expected " + section.peek());
            }
        } catch (EmptyStackException ex) {
            throw new IllegalArgumentException("No open sections left");
        }

        section.pop();

        indent();
        pw.print("}\r\n");
    }

    public void put(String key, Object value) {
        indent();
        pw.printf("\"%s\" \"%s\"\r\n", key, value);
    }

    public void put(String key, int value) {
        put(key, String.valueOf(value));
    }

    public void put(String key, long value) {
        put(key, String.valueOf(value));
    }

    public void put(String key, float value) {
        put(key, formatFloat(value));
    }

    public void put(String key, double value) {
        put(key, formatFloat(value));
    }

    public void put(String key, boolean value) {
        put(key, value ? "1" : "0");
    }

    public void put(String key, char value) {
        put(key, String.valueOf(value));
    }

    public void put(String key, Vector3f v, int p) {
        put(key, formatVector3f(v, p));
    }

    public void put(String key, Vector3f v) {
        put(key, formatVector3f(v, 0));
    }

    public void put(String key, Vector3f v1, Vector3f v2, Vector3f v3) {
        StringBuilder sb = new StringBuilder();

        sb.append(formatVector3f(v1, 1));
        sb.append(' ');
        sb.append(formatVector3f(v2, 1));
        sb.append(' ');
        sb.append(formatVector3f(v3, 1));

        put(key, sb.toString());
    }

    public void put(String key, TextureAxis axis) {
        put(key, formatTextureAxis(axis));
    }

    public void put(Map<String, String> stringMap) {
        for (String key : stringMap.keySet()) {
            put(key, stringMap.get(key));
        }
    }

    public void put(Entity entity) {
        for (Map.Entry<String, String> kv : entity.getEntrySet()) {
            put(kv.getKey(), kv.getValue());
        }
    }

    public void put(Texture tex) {
        put("material", tex.getTexture());
        put("uaxis", tex.getUAxis());
        put("vaxis", tex.getVAxis());
        put("lightmapscale", tex.getLightmapScale());
    }

    public void put(KeyValue keyValue) {
        put(keyValue.getKey(), keyValue.getValue());
    }

    private String formatVector3f(Vector3f v, int p) {
        StringBuilder sb = new StringBuilder();

        if (p == 1) {
            sb.append('(');
        } else if (p == 2) {
            sb.append('[');
        }

        if (!v.isValid()) {
            L.log(Level.WARNING, "Invalid vector: {0}", v);
            sb.append("0 0 0");
        } else {
            sb.append(formatFloat(v.x)).append(' ');
            sb.append(formatFloat(v.y)).append(' ');
            sb.append(formatFloat(v.z));
        }

        if (p == 1) {
            sb.append(')');
        } else if (p == 2) {
            sb.append(']');
        }

        return sb.toString();
    }

    private String formatTextureAxis(TextureAxis tx) {
        StringBuilder sb = new StringBuilder();
        sb.append('[');

        if (!tx.axis.isValid()) {
            L.log(Level.WARNING, "Invalid vector: {0}", tx.axis);
            sb.append("0 0 0 ");
        } else {
            sb.append(formatFloat(tx.axis.x)).append(' ');
            sb.append(formatFloat(tx.axis.y)).append(' ');
            sb.append(formatFloat(tx.axis.z)).append(' ');
        }

        sb.append(formatFloat(tx.shift));
        sb.append("] ");
        sb.append(formatFloat(tx.tw));

        return sb.toString();
    }

    private String formatFloat(double f) {
        return decimalFormat.format(f);
    }

    @Override
    public void close() {
        pw.close();

        // stack should be empty, otherwise someone forgot to call end() at least once
        if (!section.isEmpty()) {
            StringBuilder sb = new StringBuilder();

            // get stack trace
            Collections.reverse(section);

            while (true) {
                sb.append(section.pop());

                if (section.isEmpty()) {
                    break;
                }

                sb.append(" -> ");
            }

            L.log(Level.WARNING, "Unclosed VMF chunk: {0}", sb.toString());
        }
    }
}
