package ctbrec.sites.mfc;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import org.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;

import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.io.HtmlParser;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class MyFreeCamsModel extends AbstractModel {

    private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class);

    private int uid = -1; // undefined
    private String hlsUrl;
    private double camScore;
    private int viewerCount;
    private ctbrec.sites.mfc.State state;
    private int resolution[] = new int[2];

    /**
     * This constructor exists only for deserialization. Please don't call it directly
     */
    public MyFreeCamsModel() {}

    MyFreeCamsModel(MyFreeCams site) {
        this.site = site;
    }

    @Override
    public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
        MyFreeCamsClient.getInstance().update(this);
        return state == ctbrec.sites.mfc.State.ONLINE;
    }

    @Override
    public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
        return isOnline();
    }

    @Override
    public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
        if(state == null) {
            return State.UNKNOWN;
        }

        switch(state) {
        case ONLINE:
        case RECORDING:
            return ctbrec.Model.State.ONLINE;
        case AWAY:
            return ctbrec.Model.State.AWAY;
        case PRIVATE:
            return ctbrec.Model.State.PRIVATE;
        case GROUP_SHOW:
            return ctbrec.Model.State.GROUP;
        case OFFLINE:
        case CAMOFF:
        case UNKNOWN:
            return ctbrec.Model.State.OFFLINE;
        default:
            LOG.debug("State {} is not mapped", this.state);
            return ctbrec.Model.State.UNKNOWN;
        }
    }

    @Override
    public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
        MasterPlaylist masterPlaylist = getMasterPlaylist();
        List<StreamSource> sources = new ArrayList<>();
        for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
            if (playlist.hasStreamInfo()) {
                StreamSource src = new StreamSource();
                src.bandwidth = playlist.getStreamInfo().getBandwidth();
                if(playlist.getStreamInfo().getResolution() != null) {
                    src.width = playlist.getStreamInfo().getResolution().width;
                    src.height = playlist.getStreamInfo().getResolution().height;
                } else {
                    src.width = Integer.MAX_VALUE;
                    src.height = Integer.MAX_VALUE;
                }
                String masterUrl = hlsUrl;
                String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
                String segmentUri = baseUrl + playlist.getUri();
                src.mediaPlaylistUrl = segmentUri;
                LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
                sources.add(src);
            }
        }
        if(Config.getInstance().getSettings().mfcIgnoreUpscaled) {
            return sources.stream()
                    .filter(src -> src.height != 960)
                    .collect(Collectors.toList());
        } else {
            return sources;
        }
    }

    private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
        if(getHlsUrl() == null) {
            throw new IllegalStateException("Stream url unknown");
        }
        LOG.trace("Loading master playlist {}", hlsUrl);
        Request req = new Request.Builder().url(hlsUrl).build();
        try(Response response = site.getHttpClient().execute(req)) {
            if(response.isSuccessful()) {
                InputStream inputStream = response.body().byteStream();
                PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
                Playlist playlist = parser.parse();
                MasterPlaylist master = playlist.getMasterPlaylist();
                return master;
            } else {
                throw new HttpException(response.code(), response.message());
            }
        }
    }

    private String getHlsUrl() {
        if(hlsUrl == null) {
            MyFreeCams mfc = (MyFreeCams) getSite();
            mfc.getClient().update(this);
        }
        return hlsUrl;
    }

    @Override
    public void invalidateCacheEntries() {
        resolution = null;
    }

    @Override
    public void receiveTip(Double tokens) throws IOException {
        String tipUrl = MyFreeCams.baseUrl + "/php/tip.php";
        String initUrl = tipUrl + "?request=tip&username="+getName()+"&broadcaster_id="+getUid();
        Request req = new Request.Builder().url(initUrl).build();
        try(Response resp = site.getHttpClient().execute(req)) {
            if(resp.isSuccessful()) {
                String page = resp.body().string();
                Element hiddenInput = HtmlParser.getTag(page, "input[name=token]");
                String token = hiddenInput.attr("value");
                if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
                    RequestBody body = new FormBody.Builder()
                            .add("token", token)
                            .add("broadcaster_id", Integer.toString(uid))
                            .add("tip_value", Integer.toString(tokens.intValue()))
                            .add("submit_tip", "1")
                            .add("anonymous", "")
                            .add("public", "1")
                            .add("public_comment", "1")
                            .add("hide_amount", "0")
                            .add("silent", "")
                            .add("comment", "")
                            .add("mode", "")
                            .add("submit", " Confirm & Close Window")
                            .build();
                    req = new Request.Builder()
                            .url(tipUrl)
                            .post(body)
                            .addHeader("Referer", initUrl)
                            .build();
                    try(Response response = site.getHttpClient().execute(req)) {
                        if(!response.isSuccessful()) {
                            throw new HttpException(response.code(), response.message());
                        }
                    }
                }
            } else {
                throw new HttpException(resp.code(), resp.message());
            }
        }
    }

    @Override
    public int[] getStreamResolution(boolean failFast) throws ExecutionException {
        if (!failFast && hlsUrl != null) {
            try {
                List<StreamSource> streamSources = getStreamSources();
                Collections.sort(streamSources);
                StreamSource best = streamSources.get(streamSources.size() - 1);
                resolution = new int[] { best.width, best.height };
            } catch (ParseException | PlaylistException e) {
                LOG.warn("Couldn't determine stream resolution - {}", e.getMessage());
            } catch (ExecutionException | IOException e) {
                LOG.error("Couldn't determine stream resolution", e);
            }
        }
        return resolution;
    }

    public void setStreamUrl(String hlsUrl) {
        this.hlsUrl = hlsUrl;
    }

    public String getStreamUrl() {
        return hlsUrl;
    }

    public double getCamScore() {
        return camScore;
    }

    public void setCamScore(double camScore) {
        this.camScore = camScore;
    }

    public void setMfcState(ctbrec.sites.mfc.State state) {
        this.state = state;
    }

    @Override
    public void setName(String name) {
        if(getName() != null && name != null && !getName().equals(name)) {
            LOG.debug("Model name changed {} -> {}", getName(), name);
            setUrl("https://profiles.myfreecams.com/" + name);
        }
        super.setName(name);
    }

    public void update(SessionState state, String streamUrl) {
        uid = Integer.parseInt(state.getUid().toString());
        setName(state.getNm());
        setMfcState(ctbrec.sites.mfc.State.of(state.getVs()));
        setStreamUrl(streamUrl);
        Optional<Double> camScore = Optional.ofNullable(state.getM()).map(m -> m.getCamscore());
        setCamScore(camScore.orElse(0.0));

        // preview
        String uid = state.getUid().toString();
        String uidStart = uid.substring(0, 3);
        String previewUrl = "https://img.mfcimg.com/photos2/"+uidStart+'/'+uid+"/avatar.300x300.jpg";
        if(MyFreeCamsModel.this.state == ctbrec.sites.mfc.State.ONLINE) {
            try {
                previewUrl = getLivePreviewUrl(state);
            } catch(Exception e) {
                LOG.error("Couldn't get live preview. Falling back to avatar", e);
            }
        }
        setPreview(previewUrl);

        // tags
        Optional.ofNullable(state.getM()).map((m) -> m.getTags()).ifPresent((tags) -> {
            ArrayList<String> t = new ArrayList<>();
            t.addAll(tags);
            setTags(t);
        });

        // description
        Optional.ofNullable(state.getM()).map((m) -> m.getTopic()).ifPresent((topic) -> {
            try {
                setDescription(URLDecoder.decode(topic, "utf-8"));
            } catch (UnsupportedEncodingException e) {
                LOG.warn("Couldn't url decode topic", e);
            }
        });

        viewerCount = Optional.ofNullable(state.getM()).map((m) -> m.getRc()).orElseGet(() -> 0);
    }

    private String getLivePreviewUrl(SessionState state) {
        String previewUrl;
        int userChannel = 100000000 + state.getUid();
        int camserv = state.getU().getCamserv();
        String server = Integer.toString(camserv);
        ServerConfig sc = ((MyFreeCams)site).getClient().getServerConfig();
        if(sc.isOnNgServer(state)) {
            server = sc.ngVideoServers.get(Integer.toString(camserv));
            camserv = Integer.parseInt(server.replaceAll("[^0-9]+", ""));
            previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + state.getU().getPhase()+ '_' + userChannel;
        } else if(sc.isOnWzObsVideoServer(state)) {
            server = sc.wzobsServers.get(Integer.toString(camserv));
            camserv = Integer.parseInt(server.replaceAll("[^0-9]+", ""));
            previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + state.getU().getPhase()+ '_' + userChannel;
        } else if(sc.isOnHtml5VideoServer(state)) {
            server = sc.h5Servers.get(Integer.toString(camserv));
            camserv = Integer.parseInt(server.replaceAll("[^0-9]+", ""));
            previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + userChannel;
        } else {
            if(camserv > 500) camserv -= 500;
            previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + userChannel;
        }
        return previewUrl;
    }

    @Override
    public boolean follow() {
        return ((MyFreeCams)site).getClient().follow(getUid());
    }

    @Override
    public boolean unfollow() {
        return ((MyFreeCams)site).getClient().unfollow(getUid());
    }

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    public int getViewerCount() {
        return viewerCount;
    }

    public void setViewerCount(int viewerCount) {
        this.viewerCount = viewerCount;
    }

    @Override
    public void readSiteSpecificData(JsonReader reader) throws IOException {
        reader.nextName();
        uid = reader.nextInt();
    }

    @Override
    public void writeSiteSpecificData(JsonWriter writer) throws IOException {
        writer.name("uid").value(uid);
    }
}
