package com.xnx3.weixin;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

import net.sf.json.JSONObject;

import com.xnx3.DateUtil;
import com.xnx3.Lang;
import com.xnx3.net.HttpResponse;
import com.xnx3.net.HttpUtil;
import com.xnx3.weixin.bean.AccessToken;
import com.xnx3.weixin.bean.MessageReceive;
import com.xnx3.weixin.bean.MessageReply;
import com.xnx3.weixin.bean.UserInfo;

/**
 * 微信基本操作
 * @author 管雷鸣
 * <br><b>需导入</b> 
 * <br/><i>ezmorph-1.0.6.jar</i>
 * <br/><i>json-lib-2.4-jdk15.jar</i>
 */
public class WeiXinUtil {
	private final static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";	//获取普通access_token的url
	private final static String USER_INFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";	//获取用户个人信息的url
	private final static String OAUTH2_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect";	//网页授权跳转的url
	private final static String OAUTH2_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";	//网页授权,获取access_token
	private final static String OAUTH2_USER_INFO_URL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";	//网页授权,获取用户信息
	private final static int ACCESS_TOKEN_DELAY_TIME = 5000;	//access_token获取后使用的时长,单位为秒,官方给出的access_token获取后最大有效时间是7200秒,一个access_token的有效期最大只能是7200秒之内有效,超出后就要重新获取。这里设定获取到access_token后最大持续5000秒,超过后便再次获取新的access_token
	
	private boolean debug = true;	//调试日志是否打印
	private AccessToken accessToken;	//持久化access_token数据
	private String appId;	//AppID(应用ID)
	private String appSecret;	//AppSecret(应用密钥)
	private String token;	//用户于微信公众平台双方拟定的令牌Token
	
	public WeiXinUtil(String appId, String appSecret, String token) {
		this.appId = appId;
		this.appSecret = appSecret;
		this.token = token;
	}
	
	/**
	 * 获取最新的普通access_token
	 * @return AccessToken 若返回null,则获取access_token失败
	 */
	public AccessToken getAccessToken(){
		boolean refreshToken = false;	//需重新刷新获取token,默认是不需要
		
		if(accessToken == null){
			accessToken = new AccessToken();
			refreshToken = true;
		}
		
		//是否过时,需要重新获取token
		if(DateUtil.timeForUnix10()>accessToken.getGainTime()+ACCESS_TOKEN_DELAY_TIME){
			refreshToken = true;
		}
		
		//避免一次可能网络中断,连续获取三次,减小误差
		boolean success = !refreshToken;
		int i = 0;
		for (; i < 3 && !success ; i++) {
			success = refreshAccessToken();
		}
		
		if(!success){
			debug("连续获取"+i+"次access_token,均失败!" );
			return null;
		}else{
			return accessToken;
		}
	}
	
	/**
	 * 通过openId,获取用户的信息
	 * @param openId 普通用户的标识,对当前公众号唯一
	 * @return UserInfo	<li>若返回null,则获取失败
	 * 					<li>若不为null,先判断其subscribe,若为true,已关注,则可以取到其他的信息
	 */
	public UserInfo getUserInfo(String openId){
		HttpUtil httpUtil = new HttpUtil();
		UserInfo userInfo = null;
		HttpResponse httpResponse = httpUtil.get(USER_INFO_URL.replace("ACCESS_TOKEN", getAccessToken().getAccess_token()).replace("OPENID", openId));
		JSONObject json = JSONObject.fromObject(httpResponse.getContent());
		if(json.get("subscribe") != null){
			userInfo = new UserInfo();
			userInfo.setSubscribe(json.getString("subscribe").equals("1"));
			if(userInfo.isSubscribe()){
				userInfo.setCity(json.getString("city"));
				userInfo.setCountry(json.getString("country"));
				userInfo.setHeadImgUrl(json.getString("headimgurl"));
				userInfo.setLanguage(json.getString("language"));
				userInfo.setNickname(json.getString("nickname"));
				userInfo.setOpenid(json.getString("openid"));
				userInfo.setProvince(json.getString("province"));
				userInfo.setSex(json.getInt("sex"));
				userInfo.setSubscribeTime(json.getInt("subscribe_time"));
				userInfo.setUnionid(json.getString("unionid"));
				userInfo.setRemark(json.getString("remark"));
				userInfo.setGroupid(json.getInt("groupid"));
				userInfo.setSubscribeScene(json.getString("subscribe_scene"));
				userInfo.setQr_scene(json.getString("qr_scene"));
				userInfo.setQrSceneStr(json.getString("qr_scene_str"));
			}
		}else{
			debug("获取用户信息失败!用户openid:"+openId+",微信回执:"+httpResponse.getContent());
		}
		
		return userInfo;
	}
	
	/**
	 * 刷新重新获取access_token
	 * @return 获取成功|失败
	 */
	private boolean refreshAccessToken(){
		HttpUtil httpUtil = new HttpUtil();
		HttpResponse httpResponse = httpUtil.get(ACCESS_TOKEN_URL.replace("APPID", this.appId).replace("APPSECRET", this.appSecret));
		JSONObject json = JSONObject.fromObject(httpResponse.getContent());
		if(json.get("errcode") == null){
			//没有出错,获取access_token成功
			accessToken.setAccess_token(json.getString("access_token"));
			accessToken.setExpires_in(json.getInt("expires_in"));
			return true;
		}else{
			debug("获取access_token失败!返回值:"+httpResponse.getContent());
			return false;
		}
	}
	
	/**
	 * 获取网页授权的URL跳转地址
	 * @param redirectUri 授权后重定向的回调链接地址,无需URL转码,原始url
	 * @param scope 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息)
	 * @param state 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
	 * @return url地址
	 */
	public String getOauth2Url(String redirectUri,String scope,String state){
		return OAUTH2_URL.replace("APPID", this.appId).replace("REDIRECT_URI", Lang.stringToUrl(redirectUri)).replace("SCOPE", scope).replace("STATE", state);
	}
	
	/**
	 * 获取网页授权的URL跳转地址,弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息
	 * @param redirectUri 授权后重定向的回调链接地址,无需URL转码,原始url
	 * @return url地址
	 */
	public String getOauth2SimpleUrl(String redirectUri){
		return getOauth2Url(redirectUri, "snsapi_userinfo", "STATE");
	}
	
	/**
	 * 获取网页授权的URL跳转地址,不会出现授权页面,只能拿到用户openid
	 * @param redirectUri 授权后重定向的回调链接地址,无需URL转码,原始url
	 * @return url地址
	 */
	public String getOauth2ExpertUrl(String redirectUri){
		return getOauth2Url(redirectUri, "snsapi_base", "STATE");
	}
	
	/**
	 * 获取网页授权,获取用户的openid
	 * @param code 如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE,授权成功会get方式传过来
	 * @return 用户openid 若为null,则获取失败
	 */
	public String getOauth2OpenId(String code){
		HttpUtil httpUtil = new HttpUtil();
		UserInfo userInfo = null;
		HttpResponse httpResponse = httpUtil.get(OAUTH2_ACCESS_TOKEN_URL.replace("APPID", this.appId).replace("SECRET", this.appSecret).replace("CODE", code));
		JSONObject json = JSONObject.fromObject(httpResponse.getContent());
		if(json.get("errcode") == null){
			//没有出错,获取网页access_token成功
			return json.getString("openid");
		}else{
			debug("获取网页授权access_token失败!返回值:"+httpResponse.getContent());
		}
		
		return null;
	}
	
	/**
	 * 网页授权获取用户的个人信息
	 * @param code 如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE,授权成功会get方式传过来
	 * @return	<li>若成功,返回{@link UserInfo} (无 subscribeTime 项)
	 * 			<li>若失败,返回null
	 */
	public UserInfo getOauth2UserInfo(String code){
		HttpUtil httpUtil = new HttpUtil();
		HttpResponse httpResponse = httpUtil.get(OAUTH2_ACCESS_TOKEN_URL.replace("APPID", this.appId).replace("SECRET", this.appSecret).replace("CODE", code));
		JSONObject json = JSONObject.fromObject(httpResponse.getContent());
		if(json.get("errcode") == null){
			//没有出错,获取网页access_token成功
			HttpResponse res = httpUtil.get(OAUTH2_USER_INFO_URL.replace("ACCESS_TOKEN", json.getString("access_token")).replace("OPENID", json.getString("openid")));
			JSONObject j = JSONObject.fromObject(res.getContent());
			if(j.get("errcode") == null){
				UserInfo userInfo = new UserInfo();
				userInfo.setCity(j.getString("city"));
				userInfo.setOpenid(j.getString("openid"));
				userInfo.setNickname(j.getString("nickname"));
				userInfo.setSex(j.getInt("sex"));
				userInfo.setProvince(j.getString("province"));
				userInfo.setCountry(j.getString("country"));
				userInfo.setHeadImgUrl(j.getString("headimgurl"));
				userInfo.setLanguage("zh_CN");
				return userInfo;
			}else{
				debug("获取网页授权用户信息失败!返回值:"+res.getContent());
			}
		}else{
			debug("获取网页授权access_token失败!返回值:"+httpResponse.getContent());
		}
		
		return null;
	}
	
	/**
	 * 调试日志打印
	 * @param message 日志内容
	 */
	private void debug(String message){
		if(debug){
			System.out.println("WeiXinUtil:"+message);
		}
	}

	/**
	 * 接收xml格式消息,用户通过微信公众号发送消息,有服务器接收。这里将微信服务器推送来的消息进行格式化为 {@link MessageReceive}对象
	 * <br/>通常此会存在于一个Servlet中,用于接收微信服务器推送来的消息。例如SpringMVC中可以这样写:
	 * <br/><pre>
	 * 
	 * </pre>
	 * @param request 这里便是微信服务器接收到消息后,将消息POST提交过来的请求,会自动从request中取微信post的消息内容
	 * @return	返回 {@link MessageReceive}
	 * @throws DocumentException 
	 */
	public MessageReceive receiveMessage(HttpServletRequest request) throws DocumentException{
		StringBuffer jb = new StringBuffer();
		String line = null;
		try {
			BufferedReader reader = request.getReader();
			while ((line = reader.readLine()) != null)
				jb.append(line);
		} catch (Exception e) {
			e.printStackTrace();
		}
		String messageContent = jb.toString();
		
		return receiveMessage(messageContent);
	}
	
	
	/**
	 * 接收xml格式消息,用户通过微信公众号发送消息,有服务器接收。这里将微信服务器推送来的消息进行格式化为 {@link MessageReceive}对象
	 * <br/>通常此会存在于一个Servlet中,用于接收微信服务器推送来的消息。例如SpringMVC中可以这样写:
	 * <br/><pre>
	 * 
	 * </pre>
	 * @param messageContent 这里便是微信服务器接收到消息后,将消息POST提交过来消息内容,如:
	 * 		<pre>
	 * 			<xml><ToUserName><![CDATA[gh_674025ffa56e]]></ToUserName><FromUserName><![CDATA[open_jmQkHQEf8o3xfyjfLjKXTnE]]></FromUserName><CreateTime>1509453449</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[123]]></Content><MsgId>6483053198716493194</MsgId></xml>
	 * 		</pre>
	 * @return	返回 {@link MessageReceive}
	 * @throws DocumentException 
	 */
	public MessageReceive receiveMessage(String messageContent) throws DocumentException{
		MessageReceive mr = new MessageReceive();
		
		if(messageContent == null || messageContent.length() == 0){
			//为空,那么直接返回mr,当然,mr中的各项都是空的
			return mr;
		}
		
		mr.setReceiveBody(messageContent);
		
		Document doc = DocumentHelper.parseText(messageContent); 
		Element e = doc.getRootElement();   
		
		if(e.element("CreateTime") != null){
			mr.setCreateTime(Lang.stringToInt(e.element("CreateTime").getText(), 0));
		}
		if(e.element("FromUserName") != null){
			mr.setFromUserName(e.element("FromUserName").getText());
		}
		if(e.element("MsgType") != null){
			mr.setMsgType(e.element("MsgType").getText());
		}
		if(e.element("ToUserName") != null){
			mr.setToUserName(e.element("ToUserName").getText());
		}
		if(e.element("MsgId") != null){
			mr.setMsgId(e.element("MsgId").getText());
		}
		if(e.element("Content") != null){
			mr.setContent(e.element("Content").getText());
		}
		if(e.element("Description") != null){
			mr.setDescription(e.element("Description").getText());
		}
		if(e.element("Format") != null){
			mr.setFormat(e.element("Format").getText());
		}
		if(e.element("MediaId") != null){
			mr.setMediaId(e.element("MediaId").getText());
		}
		if(e.element("PicUrl") != null){
			mr.setPicUrl(e.element("PicUrl").getText());
		}
		if(e.element("ThumbMediaId") != null){
			mr.setThumbMediaId(e.element("ThumbMediaId").getText());
		}
		if(e.element("Title") != null){
			mr.setTitle(e.element("Title").getText());
		}
		if(e.element("Url") != null){
			mr.setUrl(e.element("Url").getText());
		}
		if(e.element("Event") != null){
			mr.setEvent(e.element("Event").getText());
		}
		
		if(e.element("EventKey") != null){
			mr.setEventKey(e.element("EventKey").getText());
		}
		if(e.element("Ticket") != null){
			mr.setTicket(e.element("Ticket").getText());
		}
		
		return mr;
	}
	
	/**
	 * 微信服务器接收消息或者事件后,推送到我们的服务器。我们服务器会自动处理并给微信服务器返回一个响应:微信公众号会自动给这个用户发送一条文字消息
	 * <br/>相当于:
	 * <pre>
	 * 	MessageReply messageReply = new MessageReply(messageReceive.getFromUserName(), messageReceive.getToUserName());
	 *	messageReply.replyText(response, content);
	 * </pre>
	 * @param response {@link HttpServletResponse}响应,输出返回值给微信服务器。
	 * @param messageReceive 使用{@link #receiveMessage(HttpServletRequest)}方法获取到的 {@link MessageReceive}。这里面可以拿到是要回复给哪个用户。
	 * @param content 微信公众号自动给触发此响应的用户发送的文字消息,这里便是文字消息的内容
	 */
	public void autoReplyText(HttpServletResponse response, MessageReceive messageReceive, String content){
		MessageReply messageReply = new MessageReply(messageReceive.getFromUserName(), messageReceive.getToUserName());
		messageReply.replyText(response, content);
	}
	
	
	/**
	 * 微信公众号开发,需首先填入与微信服务器交互的我方URL地址, 填写的URL需要正确响应微信发送的Token验证。这里便是接入时的验证的作用
	 * <br/>使用时,如 SpringMVC 中:
	 * <br/><pre>
	 * 	@RequestMapping("weixin")
	 *	public void verify(HttpServletRequest request, HttpServletResponse response){
	 *		WeiXinUtil.joinVerify(request, response);
	 *	}
	 * </pre>
	 * @param request {@link HttpServletRequest}
	 * @param response {@link HttpServletResponse}
	 */
	public void joinVerify(HttpServletRequest request, HttpServletResponse response){
		response.setContentType("text/html");
		PrintWriter out = null;
		try {
			out = response.getWriter();
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		String signature = request.getParameter("signature");
		String timestamp = request.getParameter("timestamp");
		String nonce = request.getParameter("nonce");
		String echostr = request.getParameter("echostr");
		
		String reSignature = null;
		try {
			String[] str = { token, timestamp, nonce };
			Arrays.sort(str);
			String bigStr = str[0] + str[1] + str[2];
			reSignature = new SHA1().getDigestOfString(bigStr.getBytes()).toLowerCase();
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (null != reSignature && reSignature.equals(signature)) {
			//请求来自微信
			out.print(echostr);
		} else {
			out.print("error request! the request is not from weixin server");
		}
		out.flush();
		out.close();
	}
	
}