import itertools
import re
import time
from copy import deepcopy
from dataclasses import dataclass
from functools import wraps

import numpy as np
from numba import jit
from tqdm import tqdm
from tqdm.contrib.concurrent import process_map

"""
硬编码的刷新权重和掉落数据
数据来源 https://azur-stats.lyoko.io/, 2022-01-02, 约5w6样本
默认二三期全毕业，二三期定向的权重增加到四期上
假设二三期的项目刷新和四期一样，但没有收益
"""
# 索引，期数，名称，出现权重，彩图纸掉落，彩图纸掉落，金图纸掉落，金图纸掉落，金图纸掉落，彩装备掉落
PROJECT_TABLE = """
0	4	B-4	58.42861987	0	0	0.549	0.549	0.549	0.0588
1	4	B-4	58.42861987	0	0	0.549	0.549	0.549	0.0588
2	4	B-4	58.42861987	0	0	0.549	0.549	0.549	0.0588
3	4	B-4	58.42861987	0	0	0.549	0.549	0.549	0.0588
4	4	B-4	58.42861987	0	0	0.549	0.549	0.549	0.0588
5	4	B-4	58.42861987	0	0	0.549	0.549	0.549	0.0588
6	4	B-4	58.42861987	0	0	0.549	0.549	0.549	0.0588
7	4	B-4	58.42861987	0	0	0.549	0.549	0.549	0.0588
8	4	B-4	58.42861987	0	0	0.549	0.549	0.549	0.0588
9	4	B-4	29.21430994	0	0	0.549	0.549	0.549	0.0588
10	4	B-4	29.21430994	0	0	0.549	0.549	0.549	0.0588
11	4	C-6	303.3660224	0	0	0	0	0	0.0559
12	4	C-8	205.3100712	0.0471	0.0471	0.17	0.17	0.17	0.0959
13	4	C-12	155.8916073	0.077	0.077	0.245666667	0.245666667	0.245666667	0.122
14	4	C-6	303.3660224	0	0	0	0	0	0.0559
15	4	C-8	205.3100712	0.0471	0.0471	0.17	0.17	0.17	0.0959
16	4	C-12	155.8916073	0.077	0.077	0.245666667	0.245666667	0.245666667	0.122
17	4	Agir-0.5	25.39955239	4.443	0	0	0	0	0.113
18	4	Agir-2.5	766.6697864	1.119	0	0	0	0	0.0395
19	4	Agir-5	485.7871007	1.95	0	0	0	0	0.0586
20	4	Agir-8	200.3313937	3.195	0	0	0	0	0.0842
21	4	Hakuryu-0.5	25.39955239	0	4.443	0	0	0	0.113
22	4	Hakuryu-2.5	766.6697864	0	1.119	0	0	0	0.0395
23	4	Hakuryu-5	485.7871007	0	1.95	0	0	0	0.0586
24	4	Hakuryu-8	200.3313937	0	3.195	0	0	0	0.0842
25	4	Anchorage-0.5	25.39955239	0	0	8.631	0	0	0.0383
26	4	Anchorage-2.5	766.6697864	0	0	2.177	0	0	0.0508
27	4	Anchorage-5	485.7871007	0	0	3.669	0	0	0.0893
28	4	Anchorage-8	200.3313937	0	0	5.753	0	0	0.354
29	4	August-0.5	25.39955239	0	0	0	8.631	0	0.133
30	4	August-2.5	766.6697864	0	0	0	2.177	0	0.0383
31	4	August-5	485.7871007	0	0	0	3.669	0	0.0508
32	4	August-8	200.3313937	0	0	0	5.753	0	0.0893
33	4	Marcopolo-0.5	25.39955239	0	0	0	0	8.631	0.133
34	4	Marcopolo-2.5	766.6697864	0	0	0	0	2.177	0.0383
35	4	Marcopolo-5	485.7871007	0	0	0	0	3.669	0.0508
36	4	Marcopolo-8	200.3313937	0	0	0	0	5.753	0.0893
37	4	E-2	203.9216684	0	0	0	0	0	0.0253
38	4	E-2	203.9216684	0	0	0	0	0	0.0253
39	4	G-1.5	582.001119	0.075	0.075	0.274	0.274	0.274	0.0195
40	4	G-2.5	402.5500509	0.1125	0.1125	0.384666667	0.384666667	0.384666667	0.0337
41	4	G-4	305.1449135	0.1855	0.1855	0.691333333	0.691333333	0.691333333	0.119
42	4	G-1.5	582.001119	0.075	0.075	0.274	0.274	0.274	0.0195
43	4	G-2.5	402.5500509	0.1125	0.1125	0.384666667	0.384666667	0.384666667	0.0337
44	4	G-4	305.1449135	0.1855	0.1855	0.691333333	0.691333333	0.691333333	0.119
45	4	H-0.5	13.18982706	0.46	0.46	1.638666667	1.638666667	1.638666667	0
46	4	H-1	608.5109359	0.3035	0.3035	0.914666667	0.914666667	0.914666667	0
47	4	H-2	394.3497965	0.4425	0.4425	1.355	1.355	1.355	0
48	4	H-4	151.9433367	0.608	0.608	1.794333333	1.794333333	1.794333333	0
49	4	H-0.5	13.18982706	0.46	0.46	1.638666667	1.638666667	1.638666667	0
50	4	H-1	608.5109359	0.3035	0.3035	0.914666667	0.914666667	0.914666667	0
51	4	H-2	394.3497965	0.4425	0.4425	1.355	1.355	1.355	0
52	4	H-4	151.9433367	0.608	0.608	1.794333333	1.794333333	1.794333333	0
53	4	Q-0.5	35.35220753	0	0	0	0	0	0.354
54	4	Q-1	204.8414852	0	0	0	0	0	0.0418
55	4	Q-2	104.3558291	0	0	0	0	0	0.0754
56	4	Q-4	51.26677518	0	0	0	0	0	0.156
57	4	Q-0.5	35.35220753	0	0	0	0	0	0.354
58	4	Q-1	204.8414852	0	0	0	0	0	0.0418
59	4	Q-2	104.3558291	0	0	0	0	0	0.0754
60	4	Q-4	51.26677518	0	0	0	0	0	0.156
61	4	Q-0.5	35.35220753	0	0	0	0	0	0.354
62	4	Q-1	204.8414852	0	0	0	0	0	0.0418
63	4	Q-2	104.3558291	0	0	0	0	0	0.0754
64	4	Q-4	51.26677518	0	0	0	0	0	0.156
65	4	Q-0.5	35.35220753	0	0	0	0	0	0.354
66	4	Q-1	204.8414852	0	0	0	0	0	0.0418
67	4	Q-2	104.3558291	0	0	0	0	0	0.0754
68	4	Q-4	51.26677518	0	0	0	0	0	0.156
69	4	Q-0.5	35.35220753	0	0	0	0	0	0.354
70	4	Q-1	204.8414852	0	0	0	0	0	0.0418
71	4	Q-2	104.3558291	0	0	0	0	0	0.0754
72	4	Q-4	51.26677518	0	0	0	0	0	0.156
73	4	T-3	261.8007121	0	0	0	0	0	0.0333
74	4	T-4	182.1410987	0	0	0	0	0	0.037
75	4	T-6	107.3408952	0	0	0	0	0	0.19
76	2	B-4	63.85544525	0	0	0	0	0	0
77	2	B-4	63.85544525	0	0	0	0	0	0
78	2	B-4	63.85544525	0	0	0	0	0	0
79	2	B-4	63.85544525	0	0	0	0	0	0
80	2	B-4	63.85544525	0	0	0	0	0	0
81	2	B-4	63.85544525	0	0	0	0	0	0
82	2	B-4	63.85544525	0	0	0	0	0	0
83	2	B-4	63.85544525	0	0	0	0	0	0
84	2	B-4	63.85544525	0	0	0	0	0	0
85	2	B-4	31.92772262	0	0	0	0	0	0
86	2	B-4	31.92772262	0	0	0	0	0	0
87	2	C-6	331.5425296	0	0	0	0	0	0
88	2	C-8	224.3791833	0	0	0	0	0	0
89	2	C-12	170.3707535	0	0	0	0	0	0
90	2	C-6	331.5425296	0	0	0	0	0	0
91	2	C-8	224.3791833	0	0	0	0	0	0
92	2	C-12	170.3707535	0	0	0	0	0	0
93	2	E-2	222.8618262	0	0	0	0	0	0
94	2	E-2	222.8618262	0	0	0	0	0	0
95	2	G-1.5	636.0571355	0	0	0	0	0	0
96	2	G-2.5	439.9387285	0	0	0	0	0	0
97	2	G-4	333.4866434	0	0	0	0	0	0
98	2	G-1.5	636.0571355	0	0	0	0	0	0
99	2	G-2.5	439.9387285	0	0	0	0	0	0
100	2	G-4	333.4866434	0	0	0	0	0	0
101	2	H-0.5	14.41489259	0	0	0	0	0	0
102	2	H-1	665.0291729	0	0	0	0	0	0
103	2	H-2	430.976838	0	0	0	0	0	0
104	2	H-4	166.0557692	0	0	0	0	0	0
105	2	H-0.5	14.41489259	0	0	0	0	0	0
106	2	H-1	665.0291729	0	0	0	0	0	0
107	2	H-2	430.976838	0	0	0	0	0	0
108	2	H-4	166.0557692	0	0	0	0	0	0
109	2	Q-0.5	38.63570553	0	0	0	0	0	0
110	2	Q-1	223.8670753	0	0	0	0	0	0
111	2	Q-2	114.0483541	0	0	0	0	0	0
112	2	Q-4	56.02841146	0	0	0	0	0	0
113	2	Q-0.5	38.63570553	0	0	0	0	0	0
114	2	Q-1	223.8670753	0	0	0	0	0	0
115	2	Q-2	114.0483541	0	0	0	0	0	0
116	2	Q-4	56.02841146	0	0	0	0	0	0
117	2	Q-0.5	38.63570553	0	0	0	0	0	0
118	2	Q-1	223.8670753	0	0	0	0	0	0
119	2	Q-2	114.0483541	0	0	0	0	0	0
120	2	Q-4	56.02841146	0	0	0	0	0	0
121	2	Q-0.5	38.63570553	0	0	0	0	0	0
122	2	Q-1	223.8670753	0	0	0	0	0	0
123	2	Q-2	114.0483541	0	0	0	0	0	0
124	2	Q-4	56.02841146	0	0	0	0	0	0
125	2	Q-0.5	38.63570553	0	0	0	0	0	0
126	2	Q-1	223.8670753	0	0	0	0	0	0
127	2	Q-2	114.0483541	0	0	0	0	0	0
128	2	Q-4	56.02841146	0	0	0	0	0	0
129	2	T-3	286.1166509	0	0	0	0	0	0
130	2	T-4	199.0582865	0	0	0	0	0	0
131	2	T-6	117.3106719	0	0	0	0	0	0
132	3	B-4	63.20796608	0	0	0	0	0	0
133	3	B-4	63.20796608	0	0	0	0	0	0
134	3	B-4	63.20796608	0	0	0	0	0	0
135	3	B-4	63.20796608	0	0	0	0	0	0
136	3	B-4	63.20796608	0	0	0	0	0	0
137	3	B-4	63.20796608	0	0	0	0	0	0
138	3	B-4	63.20796608	0	0	0	0	0	0
139	3	B-4	63.20796608	0	0	0	0	0	0
140	3	B-4	63.20796608	0	0	0	0	0	0
141	3	B-4	31.60398304	0	0	0	0	0	0
142	3	B-4	31.60398304	0	0	0	0	0	0
143	3	C-6	328.1807665	0	0	0	0	0	0
144	3	C-8	222.1040313	0	0	0	0	0	0
145	3	C-12	168.6432343	0	0	0	0	0	0
146	3	C-6	328.1807665	0	0	0	0	0	0
147	3	C-8	222.1040313	0	0	0	0	0	0
148	3	C-12	168.6432343	0	0	0	0	0	0
149	3	E-2	220.6020598	0	0	0	0	0	0
150	3	E-2	220.6020598	0	0	0	0	0	0
151	3	G-1.5	629.6076661	0	0	0	0	0	0
152	3	G-2.5	435.4778534	0	0	0	0	0	0
153	3	G-4	330.1051674	0	0	0	0	0	0
154	3	G-1.5	629.6076661	0	0	0	0	0	0
155	3	G-2.5	435.4778534	0	0	0	0	0	0
156	3	G-4	330.1051674	0	0	0	0	0	0
157	3	H-0.5	14.26872898	0	0	0	0	0	0
158	3	H-1	658.2859339	0	0	0	0	0	0
159	3	H-2	426.6068344	0	0	0	0	0	0
160	3	H-4	164.3720029	0	0	0	0	0	0
161	3	H-0.5	14.26872898	0	0	0	0	0	0
162	3	H-1	658.2859339	0	0	0	0	0	0
163	3	H-2	426.6068344	0	0	0	0	0	0
164	3	H-4	164.3720029	0	0	0	0	0	0
165	3	Q-0.5	38.24394859	0	0	0	0	0	0
166	3	Q-1	221.5971159	0	0	0	0	0	0
167	3	Q-2	112.8919307	0	0	0	0	0	0
168	3	Q-4	55.46029657	0	0	0	0	0	0
169	3	Q-0.5	38.24394859	0	0	0	0	0	0
170	3	Q-1	221.5971159	0	0	0	0	0	0
171	3	Q-2	112.8919307	0	0	0	0	0	0
172	3	Q-4	55.46029657	0	0	0	0	0	0
173	3	Q-0.5	38.24394859	0	0	0	0	0	0
174	3	Q-1	221.5971159	0	0	0	0	0	0
175	3	Q-2	112.8919307	0	0	0	0	0	0
176	3	Q-4	55.46029657	0	0	0	0	0	0
177	3	Q-0.5	38.24394859	0	0	0	0	0	0
178	3	Q-1	221.5971159	0	0	0	0	0	0
179	3	Q-2	112.8919307	0	0	0	0	0	0
180	3	Q-4	55.46029657	0	0	0	0	0	0
181	3	Q-0.5	38.24394859	0	0	0	0	0	0
182	3	Q-1	221.5971159	0	0	0	0	0	0
183	3	Q-2	112.8919307	0	0	0	0	0	0
184	3	Q-4	55.46029657	0	0	0	0	0	0
185	3	T-3	283.2154955	0	0	0	0	0	0
186	3	T-4	197.0398824	0	0	0	0	0	0
187	3	T-6	116.1211694	0	0	0	0	0	0
"""
PROJECT_TABLE_S4 = """
0	4	B-4	185.4920312	0	0	0.549	0.549	0.549	0.0588
1	4	B-4	185.4920312	0	0	0.549	0.549	0.549	0.0588
2	4	B-4	185.4920312	0	0	0.549	0.549	0.549	0.0588
3	4	B-4	185.4920312	0	0	0.549	0.549	0.549	0.0588
4	4	B-4	185.4920312	0	0	0.549	0.549	0.549	0.0588
5	4	B-4	185.4920312	0	0	0.549	0.549	0.549	0.0588
6	4	B-4	185.4920312	0	0	0.549	0.549	0.549	0.0588
7	4	B-4	185.4920312	0	0	0.549	0.549	0.549	0.0588
8	4	B-4	185.4920312	0	0	0.549	0.549	0.549	0.0588
9	4	B-4	92.7460156	0	0	0.549	0.549	0.549	0.0588
10	4	B-4	92.7460156	0	0	0.549	0.549	0.549	0.0588
11	4	C-6	963.0893184	0	0	0	0	0	0.0559
12	4	C-8	651.7932859	0.0471	0.0471	0.17	0.17	0.17	0.0959
13	4	C-12	494.9055951	0.077	0.077	0.245666667	0.245666667	0.245666667	0.122
14	4	C-6	963.0893184	0	0	0	0	0	0.0559
15	4	C-8	651.7932859	0.0471	0.0471	0.17	0.17	0.17	0.0959
16	4	C-12	494.9055951	0.077	0.077	0.245666667	0.245666667	0.245666667	0.122
17	4	Agir-0.5	25.39955239	4.443	0	0	0	0	0.113
18	4	Agir-2.5	766.6697864	1.119	0	0	0	0	0.0395
19	4	Agir-5	485.7871007	1.95	0	0	0	0	0.0586
20	4	Agir-8	200.3313937	3.195	0	0	0	0	0.0842
21	4	Hakuryu-0.5	25.39955239	0	4.443	0	0	0	0.113
22	4	Hakuryu-2.5	766.6697864	0	1.119	0	0	0	0.0395
23	4	Hakuryu-5	485.7871007	0	1.95	0	0	0	0.0586
24	4	Hakuryu-8	200.3313937	0	3.195	0	0	0	0.0842
25	4	Anchorage-0.5	25.39955239	0	0	8.631	0	0	0.0383
26	4	Anchorage-2.5	766.6697864	0	0	2.177	0	0	0.0508
27	4	Anchorage-5	485.7871007	0	0	3.669	0	0	0.0893
28	4	Anchorage-8	200.3313937	0	0	5.753	0	0	0.354
29	4	August-0.5	25.39955239	0	0	0	8.631	0	0.133
30	4	August-2.5	766.6697864	0	0	0	2.177	0	0.0383
31	4	August-5	485.7871007	0	0	0	3.669	0	0.0508
32	4	August-8	200.3313937	0	0	0	5.753	0	0.0893
33	4	Marcopolo-0.5	25.39955239	0	0	0	0	8.631	0.133
34	4	Marcopolo-2.5	766.6697864	0	0	0	0	2.177	0.0383
35	4	Marcopolo-5	485.7871007	0	0	0	0	3.669	0.0508
36	4	Marcopolo-8	200.3313937	0	0	0	0	5.753	0.0893
37	4	E-2	647.3855544	0	0	0	0	0	0.0253
38	4	E-2	647.3855544	0	0	0	0	0	0.0253
39	4	G-1.5	1847.665921	0.075	0.075	0.274	0.274	0.274	0.0195
40	4	G-2.5	1277.966633	0.1125	0.1125	0.384666667	0.384666667	0.384666667	0.0337
41	4	G-4	968.7367243	0.1855	0.1855	0.691333333	0.691333333	0.691333333	0.119
42	4	G-1.5	1847.665921	0.075	0.075	0.274	0.274	0.274	0.0195
43	4	G-2.5	1277.966633	0.1125	0.1125	0.384666667	0.384666667	0.384666667	0.0337
44	4	G-4	968.7367243	0.1855	0.1855	0.691333333	0.691333333	0.691333333	0.119
45	4	H-0.5	41.87344863	0.46	0.46	1.638666667	1.638666667	1.638666667	0
46	4	H-1	1931.826043	0.3035	0.3035	0.914666667	0.914666667	0.914666667	0
47	4	H-2	1251.933469	0.4425	0.4425	1.355	1.355	1.355	0
48	4	H-4	482.3711089	0.608	0.608	1.794333333	1.794333333	1.794333333	0
49	4	H-0.5	41.87344863	0.46	0.46	1.638666667	1.638666667	1.638666667	0
50	4	H-1	1931.826043	0.3035	0.3035	0.914666667	0.914666667	0.914666667	0
51	4	H-2	1251.933469	0.4425	0.4425	1.355	1.355	1.355	0
52	4	H-4	482.3711089	0.608	0.608	1.794333333	1.794333333	1.794333333	0
53	4	Q-0.5	112.2318616	0	0	0	0	0	0.354
54	4	Q-1	650.3056765	0	0	0	0	0	0.0418
55	4	Q-2	331.2961139	0	0	0	0	0	0.0754
56	4	Q-4	162.7554832	0	0	0	0	0	0.156
57	4	Q-0.5	112.2318616	0	0	0	0	0	0.354
58	4	Q-1	650.3056765	0	0	0	0	0	0.0418
59	4	Q-2	331.2961139	0	0	0	0	0	0.0754
60	4	Q-4	162.7554832	0	0	0	0	0	0.156
61	4	Q-0.5	112.2318616	0	0	0	0	0	0.354
62	4	Q-1	650.3056765	0	0	0	0	0	0.0418
63	4	Q-2	331.2961139	0	0	0	0	0	0.0754
64	4	Q-4	162.7554832	0	0	0	0	0	0.156
65	4	Q-0.5	112.2318616	0	0	0	0	0	0.354
66	4	Q-1	650.3056765	0	0	0	0	0	0.0418
67	4	Q-2	331.2961139	0	0	0	0	0	0.0754
68	4	Q-4	162.7554832	0	0	0	0	0	0.156
69	4	Q-0.5	112.2318616	0	0	0	0	0	0.354
70	4	Q-1	650.3056765	0	0	0	0	0	0.0418
71	4	Q-2	331.2961139	0	0	0	0	0	0.0754
72	4	Q-4	162.7554832	0	0	0	0	0	0.156
73	4	T-3	831.1328586	0	0	0	0	0	0.0333
74	4	T-4	578.2392675	0	0	0	0	0	0.037
75	4	T-6	340.7727365	0	0	0	0	0	0.19
"""

"""
从 Alas (https://github.com/LmeSzinc/AzurLaneAutoScript) 里复制过来的一大堆代码
只是为了能单文件运行
"""


class cached_property:
    """
    cached-property from https://github.com/pydanny/cached-property

    A property that is only computed once per instance and then replaces itself
    with an ordinary attribute. Deleting the attribute resets the property.
    Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
    """

    def __init__(self, func):
        self.func = func

    def __get__(self, obj, cls):
        if obj is None:
            return self

        value = obj.__dict__[self.func.__name__] = self.func(obj)
        return value


def timer(function):
    @wraps(function)
    def function_timer(*args, **kwargs):
        t0 = time.time()

        result = function(*args, **kwargs)
        t1 = time.time()
        print('%s: %s s' % (function.__name__, str(round(t1 - t0, 10))))
        return result

    return function_timer


class Filter:
    def __init__(self, regex, attr, preset=()):
        """
        Args:
            regex: Regular expression.
            attr: Attribute name.
            preset: Build-in string preset.
        """
        if isinstance(regex, str):
            regex = re.compile(regex)
        self.regex = regex
        self.attr = attr
        self.preset = tuple(list(p.lower() for p in preset))
        self.filter_raw = []
        self.filter = []

    def load(self, string):
        string = str(string)
        self.filter_raw = [f.strip(' \t\r\n') for f in string.split('>')]
        self.filter = [self.parse_filter(f) for f in self.filter_raw]

    def is_preset(self, filter):
        return len(filter) and filter.lower() in self.preset

    def apply(self, objs, func=None):
        """
        Args:
            objs (list): List of objects and strings
            func (callable): A function to filter object.
                Function should receive an object as arguments, and return a bool.
                True means add it to output.

        Returns:
            list: A list of objects and preset strings, such as [object, object, object, 'reset']
        """
        out = []
        for raw, filter in zip(self.filter_raw, self.filter):
            if self.is_preset(raw):
                raw = raw.lower()
                if raw not in out:
                    out.append(raw)
            else:
                for index, obj in enumerate(objs):
                    if self.apply_filter_to_obj(obj=obj, filter=filter) and obj not in out:
                        out.append(obj)

        if func is not None:
            objs, out = out, []
            for obj in objs:
                if isinstance(obj, str):
                    out.append(obj)
                elif func(obj):
                    out.append(obj)
                else:
                    # Drop this object
                    pass

        return out

    def apply_filter_to_obj(self, obj, filter):
        """
        Args:
            obj (object):
            filter (list[str]):

        Returns:
            bool: If an object satisfy a filter.
        """
        for attr, value in zip(self.attr, filter):
            if not value:
                continue
            if str(obj.__getattribute__(attr)).lower() != str(value):
                return False

        return True

    def parse_filter(self, string):
        """
        Args:
            string (str):

        Returns:
            list[strNone]:
        """
        string = string.replace(' ', '').lower()
        result = re.search(self.regex, string)

        if self.is_preset(string):
            return [string]

        if result and len(string) and result.span()[1]:
            return [result.group(index + 1) for index, attr in enumerate(self.attr)]
        else:
            print(f'Invalid filter: "{string}". This selector does not match the regex, nor a preset.')
            # Invalid filter will be ignored.
            # Return strange things and make it impossible to match
            return ['1nVa1d'] + [None] * (len(self.attr) - 1)


class SelectedGrids:
    def __init__(self, grids):
        self.grids = grids

    def __iter__(self):
        return iter(self.grids)

    def __getitem__(self, item):
        if isinstance(item, int):
            return self.grids[item]
        else:
            return SelectedGrids(self.grids[item])

    def __contains__(self, item):
        return item in self.grids

    def __str__(self):
        # return str([str(grid) for grid in self])
        return '[' + ', '.join([str(grid) for grid in self]) + ']'

    def __len__(self):
        return len(self.grids)

    def __bool__(self):
        return self.count > 0

    # def __getattr__(self, item):
    #     return [grid.__getattribute__(item) for grid in self.grids]

    @property
    def location(self):
        """
        Returns:
            list[tuple]:
        """
        return [grid.location for grid in self.grids]

    @property
    def cost(self):
        """
        Returns:
            list[int]:
        """
        return [grid.cost for grid in self.grids]

    @property
    def weight(self):
        """
        Returns:
            list[int]:
        """
        return [grid.weight for grid in self.grids]

    @property
    def count(self):
        """
        Returns:
            int:
        """
        return len(self.grids)

    def select(self, **kwargs):
        """
        Args:
            **kwargs: Attributes of Grid.

        Returns:
            SelectedGrids:
        """
        result = []
        for grid in self:
            flag = True
            for k, v in kwargs.items():
                grid_v = grid.__getattribute__(k)
                if type(grid_v) != type(v) or grid_v != v:
                    flag = False
            if flag:
                result.append(grid)

        return SelectedGrids(result)

    def filter(self, func):
        """
        Filter grids by a function.

        Args:
            func (callable): Function should receive an grid as argument, and return a bool.

        Returns:
            SelectedGrids:
        """
        return SelectedGrids([grid for grid in self if func(grid)])

    def set(self, **kwargs):
        """
        Set attribute to each grid.

        Args:
            **kwargs:
        """
        for grid in self:
            for key, value in kwargs.items():
                grid.__setattr__(key, value)

    def get(self, attr):
        """
        Get an attribute from each grid.

        Args:
            attr: Attribute name.

        Returns:
            list:
        """
        return [grid.__getattribute__(attr) for grid in self.grids]

    def call(self, func, **kwargs):
        """
        Call a function in reach grid, and get results.

        Args:
            func (str): Function name to call.
            **kwargs:

        Returns:
            list:
        """
        return [grid.__getattribute__(func)(**kwargs) for grid in self]

    def add(self, grids):
        """
        Args:
            grids(SelectedGrids):

        Returns:
            SelectedGrids:
        """
        return SelectedGrids(list(set(self.grids + grids.grids)))

    def add_by_eq(self, grids):
        """
        Another `add()` method, but de-duplicates with `__eq__` instead of `__hash__`.

        Args:
            grids(SelectedGrids):

        Returns:
            SelectedGrids:
        """
        new = []
        for grid in self.grids + grids.grids:
            if grid not in new:
                new.append(grid)

        return SelectedGrids(new)

    def intersect(self, grids):
        """
        Args:
            grids(SelectedGrids):

        Returns:
            SelectedGrids:
        """
        return SelectedGrids(list(set(self.grids).intersection(set(grids.grids))))

    def intersect_by_eq(self, grids):
        """
        Another `intersect()` method, but de-duplicates with `__eq__` instead of `__hash__`.

        Args:
            grids(SelectedGrids):

        Returns:
            SelectedGrids:
        """
        new = []
        for grid in self.grids:
            if grid in grids.grids:
                new.append(grid)

        return SelectedGrids(new)

    def delete(self, grids):
        """
        Args:
            grids(SelectedGrids):

        Returns:
            SelectedGrids:
        """
        g = [grid for grid in self.grids if grid not in grids]
        return SelectedGrids(g)


def hr0(title):
    middle = '|' + ' ' * 20 + title + ' ' * 20 + '|'
    border = '+' + '-' * (len(middle) - 2) + '+'
    print(border)
    print(middle)
    print(border)


def hr1(title):
    print('=' * 20 + ' ' + title + ' ' + '=' * 20)


def hr2(title):
    print('-' * 20 + ' ' + title + ' ' + '-' * 20)


def hr3(title):
    print('<' * 3 + ' ' + title + ' ' + '>' * 3)


FILTER_REGEX = re.compile('([s\!][1234])?'
                          '-?'
                          '(neptune|monarch|ibuki|izumo|roon|saintlouis'
                          '|seattle|georgia|kitakaze|azuma|friedrich'
                          '|gascogne|champagne|cheshire|drake|mainz|odin'
                          '|anchorage|hakuryu|agir|august|marcopolo)?'
                          '(dr|pry)?'
                          '([bcdeghqt])?'
                          '-?'
                          '(\d.\d|\d\d?)?')
FILTER_ATTR = ('series', 'ship', 'ship_rarity', 'genre', 'duration')
FILTER_PRESET = ('shortest', 'cheapest', 'reset')
FILTER = Filter(FILTER_REGEX, FILTER_ATTR, FILTER_PRESET)

"""
科研优化器开始
"""


def parse_value(value):
    if '.' in value:
        try:
            return float(value)
        except ValueError:
            pass
    else:
        try:
            return int(value)
        except ValueError:
            pass
    return value


def parse_text_table(string, data_class):
    out = []
    for row in string.split('\n'):
        row = row.strip(' \r\n\t')
        if not len(row):
            continue
        row = row.split('\t')
        out.append(data_class(*row))
    return SelectedGrids(out)


@dataclass()
class Research:
    """
    储存每个科研项目的信息
    """
    index: int
    series: str
    name: str
    weight: float
    bp_Agir: float
    bp_Hakuryu: float
    bp_Anchorage: float
    bp_August: float
    bp_Marcopolo: float
    bp_Tenrai: float

    def __post_init__(self):
        # 转换变量类型
        for k, v in self.__dict__.items():
            self.__setattr__(k, parse_value(v))
        # 构造科研过滤器需要的对象属性
        self.genre, self.duration = self.name.split('-')
        self.duration = str(self.duration)
        if self.series == 4:
            self.series = f'S{self.series}'
        else:
            self.series = f'!4'
        if self.genre in ['Agir', 'Hakuryu']:
            self.ship = self.genre
            self.ship_rarity = 'dr'
            self.genre = 'D'
        elif self.genre in ['Anchorage', 'August', 'Marcopolo']:
            self.ship = self.genre
            self.ship_rarity = 'pry'
            self.genre = 'D'
        else:
            self.ship = ''
            self.ship_rarity = ''

    def __hash__(self):
        return hash(self.name)


PROJECTS = parse_text_table(PROJECT_TABLE, Research)
PROJECTS_S4 = parse_text_table(PROJECT_TABLE_S4, Research)


def product_dict(func):
    out = {}
    for project in PROJECTS:
        out[project.index] = func(project)
    return out


# 掉落加那么一点点，防止过滤器写错，100年都不毕业
PROJECT_DROP = product_dict(lambda project: np.array(
    [project.bp_Agir, project.bp_Hakuryu, project.bp_Anchorage, project.bp_August, project.bp_Marcopolo,
     project.bp_Tenrai]) + 0.000001)
PROJECT_DURATION = product_dict(lambda project: float(project.duration) / 24)
# 构造出掉落数据的数组，给numba
# Shape: (project_index=188, drop_items=6)
PROJECT_DROP_ARRAY = np.array(list(PROJECT_DROP.values()))
PROJECT_DURATION_ARRAY = np.array(list(PROJECT_DURATION.values()))


class ResearchPool:
    remove_projects = 'B > T > E'
    all_ships = ('Agir', 'Hakuryu', 'Anchorage', 'August', 'Marcopolo')

    def __init__(self, string):
        FILTER.load(string)
        self.filter = SelectedGrids(FILTER.apply(PROJECTS.grids))
        self.reset_index = 1000
        for index, project in enumerate(self.filter):
            if str(project) == 'reset':
                self.reset_index = index
                break

    @cached_property
    def project_select_index(self):
        """
        将过滤器字符串转换为项目选择索引，越低表示越优先选择，1000表示不选择，需要刷新

        Returns:
            np.ndarray: Shape (188,), lower index means to be selected first. 1000 for not selected projects.
        """
        out = np.ones((PROJECTS.count,), dtype=np.int64) * 1000
        for index, project in enumerate(self.filter):
            if index != self.reset_index:
                out[project.index] = index

        FILTER.load(ResearchPool.remove_projects)
        projects = FILTER.apply(PROJECTS.grids)
        for project in projects:
            out[project.index] = 1000
        return out

    @classmethod
    def cal_project_spawn_rate(cls, projects):
        """
        计算不同完成条件下的出现概率

        Returns:
            dict(tuple, np.ndarray): Key: Combinations of conditions, such as b'\x00\x00\x00\x00\x00\x00'
                value: project appear rate.
        """
        out = {}
        for condition in itertools.product([False, True], repeat=len(PROJECT_DROP[0])):
            ships = [ship for ship, con in zip(cls.all_ships, condition) if con]
            weight = np.array(projects.get('weight'))
            index = np.sum(np.array(condition) * [1, 2, 4, 8, 16, 32])
            remain = len(ships)
            if 0 < remain < 5:
                changed = []
                # 将所有四期船的概率增加到未完成的船上
                for ship in ships:
                    for project in projects.select(ship=ship):
                        weight[project.index] *= 5 / remain
                        changed.append(project.index)
                # 将已完成的科研船的定向概率归0
                for project in projects.select(genre='D'):
                    if project.index not in changed:
                        weight[project.index] = 0
            weight /= np.sum(weight)
            out[index] = weight
        return out


SPAWN_RATE = ResearchPool('reset').cal_project_spawn_rate(PROJECTS)
SPAWN_RATE_S4 = ResearchPool('reset').cal_project_spawn_rate(PROJECTS_S4)
# 构造出不同条件下的刷新概率数组，给numba
# 事先累加概率，加快 random_choice()
SPAWN_RATE = np.array([np.cumsum(SPAWN_RATE[n]) for n in range(64)])
SPAWN_RATE_S4 = np.array([np.cumsum(SPAWN_RATE_S4[n]) for n in range(64)])


@jit(nopython=True, fastmath=True)
def random_choice(size, possibility_cumsum):
    """
    numpy.random.choice()的土法实现
    因为numba不支持numpy.random.choice()的p参数（概率数组）
    https://numba.pydata.org/numba-doc/dev/reference/numpysupported.html

    Args:
        size (int): 只能生成一维数组
        possibility_cumsum (np.ndarray): 经过累加后的出现概率
    """
    rdm_unif = np.random.rand(size)
    return np.searchsorted(possibility_cumsum, rdm_unif)


@jit(nopython=True, fastmath=True)
def sample(condition, project_select_index, reset_index):
    """
    随机生成科研项目并选择

    Args:
        condition (np.ndarray):Shape: (6,) 各种物品的完成情况
        project_select_index (np.ndarray): Shape: (188,) 项目的选择优先级，越低表示越优先选择，1000表示不选择，需要刷新
        reset_index (int): 刷新所对应的优先级数值

    Returns:
        int, int: 有刷新时选择的科研项目, 无刷新时选择的科研项目
    """
    while 1:
        # 将完成情况转换成数组索引
        index = 0
        for i, c in enumerate(condition):
            if c:
                index += 2 ** i
        # 随机生成5个科研项目，包含3个四期，和2个任意
        # np.random.seed(3)
        p1, p2, p3 = random_choice(3, SPAWN_RATE_S4[index])
        p4, p5 = random_choice(2, SPAWN_RATE[index])
        # 去重
        if p1 == p4 or p2 == p4 or p3 == p4 or p1 == p5 or p2 == p5 or p3 == p5:
            continue
        # 加入刷新，1000表示刷新
        project_list = np.array([p1, p2, p3, p4, p5, 1000])
        # 将项目索引转换为过滤器索引
        f1, f2, f3, f4, f5 = np.take(project_select_index, project_list[:5])
        # print(filter_index)
        # print(project_list)

        # 无刷新时，选择的科研项目
        s_index = np.array([f1, f2, f3, f4, f5, 999])
        selected_no_reset = project_list[np.argmin(s_index)]
        # 有刷新时，选择的科研项目
        s_index = np.array([f1, f2, f3, f4, f5, reset_index])
        selected_with_reset = project_list[np.argmin(s_index)]

        return selected_with_reset, selected_no_reset


@jit(nopython=True, fastmath=True)
def events_add(rewards, condition):
    # 活动兑换蓝图给进度最慢的，有利于提高整体速度
    # 因为G系给的是随机的，早毕业的就溢出了，给进度最慢的不会溢出，就快了
    index = np.argmin(rewards[:2])
    rewards[index] += 0.5  # 15 DR blueprints in each event
    index = np.argmin(rewards[2:5])
    rewards[index + 2] += 1  # 30 PRY blueprints in each event
    return rewards


@jit(nopython=True, fastmath=True)
def simulate(project_select_index, reset_index, target, active=1., interval=0.):
    """
    模拟一个玩家做科研到毕业

    Args:
        project_select_index (np.ndarray):Shape: (188,) 项目的选择优先级，越低表示越优先选择，1000表示不选择，需要刷新
        reset_index (int): 刷新所对应的优先级数值
        target  (np.ndarray): Shape: (6,) 目标物品数量
        active (float): 每日活跃时间，单位 天，超出活跃时间后，仍在挂项目，但不再开始新项目
        interval (float): 收菜时间，单位 天，项目完成后，过多长时间才收获

    Returns:
        float, np.ndarray: 消耗时间，累计获得物品 Shape: (6,)
    """
    rewards = np.array([0., 0., 0., 0., 0., 0.])
    condition = rewards != 0  # 每样物品是否达到目标数量，True未达到，False已达到
    has_reset = True
    day_cost = 0

    while 1:
        """
        做一个科研项目
        """
        while 1:
            index, index_no_reset = sample(condition, project_select_index, reset_index)
            # print(index, index_no_reset, has_reset)
            if has_reset:
                if index == 1000:
                    has_reset = False
                    continue
                else:
                    break
            else:
                if index_no_reset == 1000:
                    # 刷新次数用完，且需要刷新时，等到明天，使用明天的刷新次数
                    day_cost = int(day_cost) + 1
                    rewards = events_add(rewards, condition)
                    has_reset = False
                    continue
                else:
                    index = index_no_reset
                    break

        """
        收获科研项目，计算收益和消耗时间
        """
        prev_day = int(day_cost)
        rewards += PROJECT_DROP_ARRAY[index]
        day_cost += PROJECT_DURATION_ARRAY[index] + interval
        # print(rewards / day_cost)
        condition = rewards < target
        new_day = int(day_cost)
        new_hour = day_cost - new_day
        # 跨天重置刷新次数
        if new_day > prev_day:
            has_reset = True
            rewards = events_add(rewards, condition)
        else:
            # 超出活跃时间
            if new_hour > active:
                day_cost = int(day_cost) + 1
                has_reset = True
                rewards = events_add(rewards, condition)

        """
        达成目标物品数量
        """
        if not np.any(condition):
            break

    return day_cost, rewards


class FilterSimulator:
    active = 24 / 24
    interval = 0 / 60 / 24
    target = np.array([513, 513, 343, 343, 343, 150])

    def __init__(self, string):
        self.string = string
        self.pool = ResearchPool(string)

    def run(self, sample_count=1000):
        day_cost = 0
        rewards = PROJECT_DROP[0] * 0
        for _ in tqdm(range(sample_count)):
            sim_day, sim_rewards = simulate(
                self.pool.project_select_index,
                self.pool.reset_index,
                target=FilterSimulator.target,
                active=FilterSimulator.active,
                interval=FilterSimulator.interval
            )
            day_cost += sim_day
            rewards += sim_rewards
        day_cost /= sample_count
        rewards /= sample_count

        hr3('End Testing')
        print(self.string)
        print(f'Average time cost: {day_cost}')
        print(f'Average rewards: {rewards}')

        return day_cost


def split_filter(string):
    if isinstance(string, list):
        return string
    return [f.strip(' \t\r\n') for f in string.split('>')]


def join_filter(selection):
    if isinstance(selection, str):
        return selection
    return ' > '.join(selection)


def beautify_filter(string):
    if isinstance(string, str):
        string = split_filter(string)
    out = ''
    for index in range(0, len(string), 8):
        row = string[index:index + 8]
        out += '    > ' + join_filter(row) + '\n'
    out = '\n    ' + out.strip('> ')
    return out


def position_change(string, position):
    selection = split_filter(string)
    selection[position], selection[position + 1] = selection[position + 1], selection[position]
    return join_filter(selection)


def position_insert(string, insert, position):
    selection = split_filter(string)
    selection.insert(position, insert)
    return join_filter(selection)


def epoch_worker(data):
    index, total, sample_count, select_index, forward_index, string = data
    hr3(f'Start Testing: {index}/{total}')
    return FilterSimulator(string).run(sample_count)


class BruteForceOptimizer:
    @timer
    def optimize(self, string, diff=10):
        for epoch in range(100):
            hr0(f'Epoch: {epoch}')
            new, diff = self.epoch(string, diff=diff)
            if new == string:
                break
            else:
                string = new
                continue

    def gen(self, string, look_forward):
        string = split_filter(string)
        yield 0, 0, join_filter(string)

        for select_index, select_item in enumerate(string):
            string_dropped = [s for s in string if s != select_item]
            for forward_index in range(1, look_forward + 1):
                forward_index = select_index - forward_index
                if forward_index < 0:
                    continue
                string_added = deepcopy(string_dropped)
                string_added.insert(forward_index, select_item)
                string_added = join_filter(string_added)
                yield select_index, select_index - forward_index, string_added

    def epoch(self, string, diff=10):
        diff = min(abs(diff), 1)
        level = np.log(diff) / np.log(10) + 1
        sample_count = int(np.power(10, 5 - level / 2))
        look_forward = int(np.power(3, level))
        look_forward = max(look_forward, 1)
        sample_count = min(max(sample_count, 10000), 300000)
        print(f'diff: {diff}, look_forward: {look_forward}, sample_count: {sample_count}')

        string_split = split_filter(string)
        string_count = len(string_split)
        all_tests = list(self.gen(string, look_forward=look_forward))
        total = len(all_tests)
        # index, total, sample_count, select_index, forward_index, string_added
        tests_data = [(index, total, sample_count, *row) for index, row in enumerate(all_tests)]

        results = process_map(epoch_worker, tests_data, max_workers=BruteForceOptimizer.process)

        day_cost = np.ones((string_count, look_forward + 1)) * 1000
        for data, result in zip(tests_data[1:], results[1:]):
            day_cost[data[3]][data[4]] = result
        day_cost[:, 0] = results[0]

        hr2('Original filter')
        print(beautify_filter(string_split))

        hr2('Move forward')
        forward = np.argmin(day_cost, axis=1)
        if look_forward == 1:
            forward[np.min(day_cost, axis=1) != np.min(day_cost)] = 0
        for index, selection, forward_index in zip(range(len(forward)), string_split, forward):
            if index == 0:
                selection = '[Original]'
            print(f'{selection.ljust(12, " ")}forward: {forward_index}, day_cost: {day_cost[index][forward_index]}')
        diff = day_cost[0][0] - np.min(day_cost)
        print(f'diff: {diff}')

        hr2('New filter')
        forward[forward > 0] += 1
        forward = np.arange(forward.shape[0]) - forward
        new_index = np.argsort(forward)
        new_filter = beautify_filter([string_split[index] for index in new_index])
        print(new_filter)

        return new_filter, diff


"""
科研设置
"""
# 去除的科研项目
# 默认去除 B/T/E，因为掉落数据样本小
# 切魔方：'B > T > E'
# 只做0.5h魔方：'B > T > E > H1 > H2 > H4'
# 不切魔方：'B > T > E > H'
ResearchPool.remove_projects = 'B > T > E > H1 > H2 > H4'
# 每日活跃时间，按天计算
# 超出活跃时间后，仍在挂项目，但不再开始新项目
FilterSimulator.active = 24 / 24
# 收菜间隔，按天计算
# 项目完成后，过多长时间才收获
FilterSimulator.interval = 0 / 60 / 24
# 科研目标
# 需要的彩图纸 彩图纸 金图纸 金图纸 金图纸 彩装备 的物品数量
# 某种图纸数量满足后，不再产生该种定向科研，图纸全满后重置
# 四期毕业：np.array([513, 513, 343, 343, 343, 100])
# 仅科研船：np.array([513, 513, 343, 343, 343, 0])
# 仅天雷：np.array([0, 0, 0, 0, 0, 150])
FilterSimulator.target = np.array([513, 513, 343, 343, 343, 100])
# 运行的进程数
# 建议为cpu的物理进程数
BruteForceOptimizer.process = 6

if __name__ == '__main__':
    """
    这个文件包含模拟器和优化器两部分，取消注释对应的代码来运行
    Alas用户运行需要额外安装numba，无指定版本
    非Alas用户运行需要python>=3.7，安装 numba numpy tqdm, 无指定版本

    过滤器与Alas内的过滤器基本相同，编写参考 https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/filter_string_cn
    但需要注意：
    - 必须有且只有一个reset
    - 不能使用Alas的预设选择，比如 shortest 需要展开成
      0.5 > 1 > 1.5 > 2 > 2.5 > 3 > 4 > 5 > 6 > 8 > 10 > 12
    - 选择数量建议为 10-24 个
      选择数量不能过少，否则毕业时间过长
      选择数量不能过多，否则优化太慢
    - 增加了 "!" 表示"非"逻辑，只能用在期数上
      比如 "!4" 表示非四期，详细参考正则表达式 FILTER_REGEX

    如果你在Alas的目录下运行，可以取消注释这些代码，把过程额外输出到log中
    """
    # from module.logger import logger
    # import builtins
    # builtins.print = logger.info
    """
    模拟大量用户使用同一个过滤器的平均毕业时间和毕业时获取物品的平均数量
    取消注释这些代码，将你的过滤器粘贴至这里，并运行，在8700k上需要约4.5分钟
    """
    simulator = FilterSimulator("""
    S4-DR0.5 > S4-PRY0.5 > S4-H0.5 > S4-Q0.5 > S4-DR2.5 > !4-0.5 > S4-G1.5 > S4-Q1
    > S4-DR5 > S4-DR8 > S4-G4 > S4-PRY2.5 > !4-1 > S4-Q2 > reset > S4-G2.5
    > S4-PRY5 > S4-PRY8 > !4-2 > !4-1.5 > S4-Q4 > !4-2.5 > !4-4 > S4-C6
    > S4-C8 > !4-6 > !4-8 > !4-12 > S4-C12
    """)
    simulator.run(sample_count=100000)
    """
    优化一个过滤器，尝试调整过滤器选择的顺序，找到满足目标条件的消耗时间最短的排列方式
    类似于早期机器学习的实现，收敛过程中，向前尝试移动的距离变短，模拟样本量增大
    取消注释这些代码并运行，在8700k上需要约1-2天
    已给出一个包含所有选项、顺序大体正确的过滤器作为开始，不需要修改
    """
    # optimizer = BruteForceOptimizer()
    # optimizer.optimize("""
    # S4-H0.5 > S4-DR0.5 > S4-PRY0.5 > S4-Q0.5 > !4-0.5 > S4-G1.5 > S4-Q1 > S4-DR2.5
    # > S4-G4 > S4-Q4 > S4-DR5 > S4-DR8 > S4-Q2 > S4-PRY2.5 > S4-G2.5 > !4-1
    # > reset > S4-PRY8 > !4-1.5 > S4-PRY5 > !4-2.5 > !4-2 > !4-4
    # > S4-C6 > !4-C8 > S4-C8 > !4-C6 > S4-C12 > !4-C12
    # """, diff=1)
