
// 사용자/IP 추적. 구버전 ~provider/analysis_user.jsx 의 이식.
//  - 기준 접속을 "같은 PC / 다른 PC / 같은 IP" 로 갈라 기간별 누적 막대로 본다
//  - 목록에서 다른 사용자나 IP 를 누르면 그것을 새 기준으로 다시 분석한다 (히스토리 < 로 복귀)
// 구버전은 원시 행 전체를 받아 JS 로 갈랐지만, 여기서는 기준이 바뀔 때마다 서버에 다시 묻는다.
const ATRACE_COLORS = {
	same:"#0072B2",		// 같은 PC
	other:"#CC79A7",	// 다른 PC
	sameip:"#E69F00"	// 같은 IP
};

class AnalyticsTrace extends React.Component {
    constructor(props) {
        super(props);

		this.Lang = props.Lang;
		this.close = props.close;

		this.state.anchor = props.anchor;

		this.refRoot = React.createRef();
		this.refChart = React.createRef();
	}

	state = {
		anchor:null,
		history:[],

		data:null,
		loading:true
	};

	async componentDidMount() {
		this.styleMain = R5.wStyle(`
/* 화면 전체를 덮는다. 우클릭을 여기서 잡아 window 까지 올라가지 않게 하려면
   패널 밖(여백)을 눌러도 이 안이어야 한다. */
#AnalyticsTrace {
	position:fixed;
	left:0;
	right:0;
	top:0;
	bottom:0;
	background-color:rgba(0,0,0,0.5);
	z-index:20;
}
#AnalyticsTrace .panel {
	position:absolute;
	left:20dp;
	right:20dp;
	top:20dp;
	bottom:20dp;
	display:flex;
	flex-direction:column;
	background:#fff;
	border:1px solid #888;
	border-radius:8dp;
	overflow:hidden;
}

#AnalyticsTrace .topbar {
	position:relative;
	flex-shrink:0;
	height:40dp;
	background:#3988ff;
}
#AnalyticsTrace .topbar .back {
	position:absolute;
	left:8dp;
	top:6dp;
	width:34dp;
	height:28dp;
	line-height:28dp;
	font-size:16dp;
	text-align:center;
	color:#fff;
	border:1px solid #fff;
	border-radius:4dp;
	cursor:pointer;
}
#AnalyticsTrace .topbar .back:hover {
	background:#fff;
	color:#3988ff;
}
#AnalyticsTrace .topbar .x {
	position:absolute;
	right:8dp;
	top:6dp;
	width:34dp;
	height:28dp;
	line-height:28dp;
	font-size:16dp;
	text-align:center;
	color:#fff;
	border:1px solid #fff;
	border-radius:4dp;
	cursor:pointer;
}
#AnalyticsTrace .topbar .x:hover {
	background:#f44;
	border-color:#f44;
}
#AnalyticsTrace .topbar .who {
	position:absolute;
	left:50dp;
	right:50dp;
	top:0;
	height:40dp;
	line-height:40dp;
	font-size:15dp;
	font-weight:bold;
	color:#fff;
	text-align:center;
	white-space:nowrap;
	overflow:hidden;
	text-overflow:ellipsis;
}

#AnalyticsTrace .body {
	position:relative;
	flex:1;
	min-height:0;
	display:flex;
	flex-direction:column;
	overflow-x:hidden;
	overflow-y:auto;
}

/* 기본이 content-box 라 padding/border 가 너비에 더해진다.
   그대로 두면 창이 좁을 때 가로로 넘친다. */
#AnalyticsTrace .tiles .tile,
#AnalyticsTrace .chartbox {
	box-sizing:border-box;
}
#AnalyticsTrace canvas {
	max-width:100%;
}

#AnalyticsTrace .tiles {
	position:relative;
	flex-shrink:0;
	display:flex;
	flex-wrap:wrap;
	gap:10dp;
	padding:10dp;
}
#AnalyticsTrace .tiles .tile {
	position:relative;
	flex:1 1 120dp;
	min-width:0;
	padding:8dp 12dp;
	border:1px solid #ddd;
	border-radius:8dp;
	background:#fafafa;
}
#AnalyticsTrace .tiles .tile .cap {
	font-size:12dp;
	line-height:16dp;
	color:#666;
}
#AnalyticsTrace .tiles .tile .num {
	font-size:22dp;
	line-height:30dp;
	font-weight:bold;
	color:#222;
}
#AnalyticsTrace .tiles .tile.warn .num {
	color:#c00;
}

#AnalyticsTrace .chartbox {
	position:relative;
	flex-shrink:0;
	height:230dp;
	margin:0 10dp 10dp 10dp;
	padding:10dp;
	border:1px solid #ddd;
	border-radius:8dp;
}
#AnalyticsTrace .chartbox .canvasbox {
	position:relative;
	height:200dp;
}

#AnalyticsTrace .tablebox {
	position:relative;
	flex-shrink:0;
	margin:0 10dp 10dp 10dp;
	/* 6열 표는 좁은 창에서 넘친다. 잘라내지 않고 표 안에서만 가로 스크롤시킨다. */
	overflow-x:auto;
}
#AnalyticsTrace .tablebox .cut {
	font-size:12dp;
	color:#c60;
	margin-bottom:4dp;
}
#AnalyticsTrace .listtable {
	width:100%;
	border-spacing:0;
	border:1px solid #888;
}
#AnalyticsTrace .listtable th {
	font-size:12dp;
	line-height:14dp;
	padding:4dp 0;
	border-right:1px solid #ccc;
	border-bottom:1px solid #888;
	background:#ddd;
}
#AnalyticsTrace .listtable td {
	height:26dp;
	text-align:center;
	font-size:13dp;
	padding:0 5dp;
	border-right:1px solid #fff;
	border-bottom:1px solid #fff;
}
#AnalyticsTrace .listtable tr.c1 td { background:rgba(0,114,178,0.14); }
#AnalyticsTrace .listtable tr.c2 td { background:rgba(204,121,167,0.18); }
#AnalyticsTrace .listtable tr.c3 td { background:rgba(230,159,0,0.18); }
#AnalyticsTrace .listtable .pnt {
	cursor:pointer;
	text-decoration:underline;
	color:#00a;
}
#AnalyticsTrace .listtable .pnt:hover {
	background-color:#fdd;
}
#AnalyticsTrace .listtable td.anon {
	color:#888;
}

#AnalyticsTrace .empty {
	position:relative;
	padding:30dp 10dp;
	font-size:14dp;
	text-align:center;
	color:#888;
}
`);

		// 우클릭으로 이 창만 닫는다 (X 버튼과 같은 동작).
		// Main 은 window 에서 contextmenu 를 받아 브레드크럼을 닫으므로,
		// 여기서 stopPropagation 으로 끊어야 뒤에 있는 접속 목록까지 닫히지 않는다.
		// window 가 아니라 이 요소에 직접 걸어야 끊는 지점이 확실하다.
		if (this.refRoot.current != null)
			this.refRoot.current.addEventListener("contextmenu", this.onContextMenu);

		this.downloadData();
    }

	componentWillUnmount() {
		if (this.refRoot.current != null)
			this.refRoot.current.removeEventListener("contextmenu", this.onContextMenu);

		this.destroyChart();

		if (this.styleMain != null) {
			this.styleMain.destroy();
			this.styleMain = null;
		}
	}

	// removeEventListener 로 떼려면 같은 함수 참조여야 하므로 필드로 둔다
	onContextMenu = (e)=>{
		e.preventDefault();
		e.stopPropagation();
		this.close();
	};

	componentDidUpdate() {
		this.drawChart();
	}

	downloadData() {
		let a = this.state.anchor;
		if (a == null)
			return;

		this.setState({ loading:true });

		let fd = new FormData();
		fd.append("unit", this.props.unit);
		if (this.props.did != null && this.props.did != "")
			fd.append("did", this.props.did);
		fd.append("mid", a.mid);
		fd.append("umuid", a.umuid == null ? "" : a.umuid);
		fd.append("ip", a.ip == null ? "" : a.ip);
		axios.post('/api/stats/trace/', fd)
			.then(res=>{
				if (res.data.code != 0){
					alert(res.data.msg);
					this.setState({ loading:false });
					return
				}
				this.setState({ data:res.data, loading:false });
			})
			.catch(e=>{ this.setState({ loading:false }); });
	}

	destroyChart() {
		if (this.chart != null) {
			this.chart.destroy();
			this.chart = null;
		}
		this.chartSign = null;
	}

	drawChart() {
		if (typeof Chart == "undefined")
			return;
		let cvs = this.refChart.current;
		let data = this.state.data;
		if (cvs == null || data == null || data.periods == null || data.periods.length == 0) {
			this.destroyChart();
			return;
		}

		let a = this.state.anchor;
		let sign = a.mid+"/"+a.umuid+"/"+a.ip+"/"+data.periods.length;
		if (this.chartSign == sign)
			return;
		this.destroyChart();
		this.chartSign = sign;

		let periods = data.periods;
		let mkset = (label, color, values)=>{
			return { label:label, data:values, backgroundColor:color,
					 borderColor:"#fff", borderWidth:1, stack:"s" }
		};

		this.chart = new Chart(cvs.getContext("2d"), {
			type:"bar",
			data:{
				labels:periods.map(p=>p.p),
				datasets:[
					mkset(this.Lang("t_same","an_page"), ATRACE_COLORS.same, periods.map(p=>p.same)),
					mkset(this.Lang("t_other","an_page"), ATRACE_COLORS.other, periods.map(p=>p.other)),
					mkset(this.Lang("t_sameip","an_page"), ATRACE_COLORS.sameip, periods.map(p=>p.sameip))
				]
			},
			options:{
				responsive:true,
				maintainAspectRatio:false,
				interaction:{ mode:"index", intersect:false },
				plugins:{
					legend:{ position:"bottom", labels:{ boxWidth:12, color:"#444" } }
				},
				scales:{
					x:{ stacked:true, grid:{ color:"#eee" }, ticks:{ color:"#666" } },
					y:{ stacked:true, beginAtZero:true, grid:{ color:"#eee" },
						ticks:{ color:"#666", precision:0 } }
				}
			}
		});
	}

	render() {
		let a = this.state.anchor;
		let data = this.state.data;
		let summary = (data == null || data.summary == null) ? {} : data.summary;
		let rows = (data == null || data.rows == null) ? [] : data.rows;
		let bCut = (data != null && summary.access > rows.length);

		let who = (a == null) ? "" : (a.mid > 0 ?
			(a.name + (data != null && data.anchor != null && data.anchor.email != null ?
				" ("+data.anchor.email+")" : "")) :
			("IP " + a.ip));

		return <div id='AnalyticsTrace' ref={this.refRoot}>
			<div className="panel">
				<div className="topbar">
					{this.state.history.length > 0 &&
						<div className="back" onClick={e=>this.back()}>&lt;</div>}
					<div className="who">{who}</div>
					<div className="x" onClick={e=>this.close()}>X</div>
				</div>

				<div className="body">
					<div className="tiles">
						<div className="tile">
							<div className="cap">{this.Lang("access","an_page")}</div>
							<div className="num">{MakeCommaString(summary.access == null ? 0 : summary.access)}</div>
						</div>
						<div className={summary.machines > 1 ? "tile warn" : "tile"}>
							<div className="cap">{this.Lang("t_pccnt","an_page")}</div>
							<div className="num">{MakeCommaString(summary.machines == null ? 0 : summary.machines)}</div>
						</div>
						<div className={summary.ips > 1 ? "tile warn" : "tile"}>
							<div className="cap">{this.Lang("t_ipcnt","an_page")}</div>
							<div className="num">{MakeCommaString(summary.ips == null ? 0 : summary.ips)}</div>
						</div>
						<div className={summary.users > 1 ? "tile warn" : "tile"}>
							<div className="cap">{this.Lang("t_usercnt","an_page")}</div>
							<div className="num">{MakeCommaString(summary.users == null ? 0 : summary.users)}</div>
						</div>
					</div>

					<div className="chartbox">
						<div className="canvasbox">
							{data != null && data.periods != null && data.periods.length > 0 &&
								<canvas ref={this.refChart} />}
						</div>
					</div>

					<div className="tablebox">
						{bCut && <div className="cut">
							{this.Lang("recent_only","an_page").replace(/%N%/g, MakeCommaString(rows.length))}
						</div>}
						<table className='listtable'>
							<thead>
								<tr>
									<th>{this.Lang("time","an_page")}</th>
									<th>{this.Lang("user","an_page")}</th>
									<th>{this.Lang("pc","an_page")}</th>
									<th>{this.Lang("version_col","an_page")}</th>
									<th>IP</th>
									<th>{this.Lang("locale","an_page")}</th>
								</tr>
							</thead>
							<tbody>
							{rows.map((r,idx)=>
								<tr className={"c"+r.cls} key={idx}>
									<td>{(new Date(r.time * 1000)).toLocaleString()}</td>
									{r.mid > 0 ?
										(this.isSameAnchor(r) ?
											<td>{r.name}</td> :
											<td className="pnt" onClick={e=>this.go(r)}>{r.name}</td>) :
										<td className="anon">{this.Lang("anon","an_page")}</td>}
									<td>{r.mname != null ? r.mname : (r.pcname != null ? r.pcname : "")}</td>
									<td>{r.lver}</td>
									{r.ip != null && r.ip != "" && r.ip != a.ip ?
										<td className="pnt" onClick={e=>this.goIP(r)}>{r.ip}</td> :
										<td>{r.ip}</td>}
									<td>{(r.country == null ? this.Lang("unknown","an_page") : r.country)
										+ (r.region == null ? "" : " "+r.region)}</td>
								</tr>)}
							</tbody>
						</table>
						{rows.length == 0 &&
							<div className="empty">{this.state.loading ? this.Lang("loading","an_page") : this.Lang("nodata","an_page")}</div>}
					</div>
				</div>
			</div>
		</div>
	}

	// 지금 보고 있는 기준과 같은 행이면 다시 눌러도 의미가 없다
	isSameAnchor(r) {
		let a = this.state.anchor;
		return (a != null && a.mid == r.mid && a.umuid == r.umuid);
	}

	pushAnchor( anchor ) {
		let history = this.state.history.slice();
		history.push(this.state.anchor);
		this.setState({ history:history, anchor:anchor, data:null }, e=>this.downloadData());
	}
	go(r) {
		this.pushAnchor({ mid:r.mid, umuid:(r.umuid == null ? "" : r.umuid),
						  ip:(r.ip == null ? "" : r.ip), name:r.name });
	}
	goIP(r) {
		this.pushAnchor({ mid:0, umuid:"", ip:r.ip, name:"IP "+r.ip });
	}
	back() {
		let history = this.state.history.slice();
		if (history.length <= 0)
			return;
		let anchor = history.pop();
		this.setState({ history:history, anchor:anchor, data:null }, e=>this.downloadData());
	}
}
