package com.sonic.common.rpc;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.sonic.common.exception.BizException;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.net.UrlEscapers;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author code
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Page<T> {
    public static final Integer MAX_PER_PAGE_COUNT = 999;
    private static final List<String> QUERY_KEYS = ImmutableList.of("pn", "ps", "sortType", "sortField");

    /** 页码 */
    @Builder.Default
    private Integer pn = 1;
    /** 每页显示条数 */
    @Builder.Default
    private Integer ps = 10;
    /** 数据列表 */
    @Builder.Default
    private List<T> datas = new ArrayList<>();
    /** 总数据条数 */
    @Builder.Default
    private Integer tc = 0;
    /** 排序方式 */
    private SortType sortType;
    /** 排序字段 */
    private String sortField;

    @AllArgsConstructor
    public enum SortType {
        ASC, DESC
    }

    public void validate() {
        if (Optional.ofNullable(pn).orElse(0) < 1) {
            throw new BizException(GlobalResultCode.INVALID_PARAMS.getErrorCode(), "当前页码不能为空或小于1");
        }
        int notNullPerPage = Optional.ofNullable(ps).orElse(0);
        if (notNullPerPage < 1) {
            throw new BizException(GlobalResultCode.INVALID_PARAMS.getErrorCode(), "每页容量不能为空或小于1");
        }
        if (notNullPerPage > MAX_PER_PAGE_COUNT) {
            throw new BizException(GlobalResultCode.INVALID_PARAMS.getErrorCode(), "每页容量不能大于1000");
        }
    }

    public String appendQueryStringToUrl(String url) {
        Preconditions.checkArgument(!Strings.isNullOrEmpty(url), "url不能为空");
        Preconditions.checkArgument(pn != null, "当前页码不能为空");
        Preconditions.checkArgument(ps != null, "每页容量不能为空");
        Preconditions.checkArgument(pn > 0, "当前页码不能小于1");
        Preconditions.checkArgument(ps > 0, "每页容量不能小于1");
        Preconditions.checkArgument(ps <= MAX_PER_PAGE_COUNT, "每页容量不能大于1000");

        JSONObject jsonObject = (JSONObject) JSON.toJSON(this);
        if (Strings.isNullOrEmpty(sortField)) {
            jsonObject.put("sortType", null);
        } else if (sortType == null) {
            // 默认排序方式为ASC
            jsonObject.put("sortType", SortType.ASC);
        }
        Function<String, String> escape = UrlEscapers.urlPathSegmentEscaper().asFunction();
        String queryString = jsonObject.entrySet().stream()
                // 仅处理分页查询条件
                .filter(p -> QUERY_KEYS.contains(p.getKey()))
                .filter(p -> p.getValue() != null)
                .map(p -> escape.apply(p.getKey() + "=" + p.getValue()))
                .collect(Collectors.joining("&"));
        return url + "?" + queryString;
    }

    /**
     * 创建一个空结果的页（便于业务上快速返回空页）
     */
    public Page<T> buildEmptyResultPage() {
        return new Page<>(pn, ps, Collections.emptyList(), 0, sortType, sortField);
    }

    /**
     * sortType默认为ASC
     *
     * @return
     */
    public SortType getSortType() {
        return Optional.ofNullable(sortType).orElse(SortType.ASC);
    }

    /**
     * 用于判断是否支持sort
     * 目前只检查了sortField，如果sortType为null，默认用ASC
     * @return boolean
     */
    @JSONField(serialize = false)
    public boolean hasSort() {
        return !Strings.isNullOrEmpty(sortField);
    }

    /**
     * 将 page 中的 datas 映射成另一个类型
     *
     * @param mapper
     * @param <R>
     * @return
     */
    public <R> Page<R> convert(Function<? super T, ? extends R> mapper) {
        Page<R> page = ((Page<R>)this);
        if (mapper != null && datas != null) {
            List<R> collect = datas.stream().map(mapper).collect(Collectors.toList());
            page.setDatas(collect);
        }
        return page;
    }

    public <U> List<T> drainAll(U req, BiFunction<? super U, Page<T>, Page<T>> function) {
        List<T> totalData = Lists.newArrayList();
        while (true) {
            Page newPage = buildEmptyResultPage();

            Page<T> result = function.apply(req, newPage);
            totalData.addAll(result.getDatas());

            if (result.getDatas().size() < result.getPs()) {
                break;
            }
            pn += 1;
        }
        return totalData;
    }

    public Page(Integer pn, Integer ps) {
        this.pn = pn;
        this.ps = ps;
    }

    public int startIndex() {
        return (getPn() - 1) * getPs();
    }

    public int endIndex() {
        return getPn() * getPs();
    }

    public boolean firstPage() {
        return getPn() <= 1;
    }

    public boolean lastPage() {
        return getPn() >= pageCount();
    }

    public int nextPage() {
        if (lastPage()) {
            return getPn();
        }
        return getPn() + 1;
    }

    public int previousPage() {
        if (firstPage()) {
            return 1;
        }
        return getPn() - 1;
    }

    public Integer getPn() {
        if (pn == null || pn == 0) {
            pn = 1;
        }
        return pn;
    }

    public Integer getPs() {
        if (ps == null || ps == 0) {
            ps = 10;
        }
        return ps;
    }

    public int pageCount() {
        if (tc % getPs() == 0) {
            return tc / getPs();
        } else {
            return tc / getPs() + 1;
        }
    }

    public Integer getTc() {
        return this.tc;
    }

    public void setPn(Integer pageNum) {
        this.pn = pageNum;
    }

    public void setPs(Integer ps) {
        this.ps = ps;
    }

    public boolean hasNextPage() {
        return getPn() < pageCount();
    }

    public boolean hasPreviousPage() {
        return getPn() > 1;
    }

    public List<T> getDatas() {
        return datas;
    }

    public void setDatas(List<T> data) {
        this.datas = data;
    }

    public void setTc(int total) {
        this.tc = total;
    }
}
